Skip to content

[management] Add custom dns zones#4849

Merged
bcmmbaga merged 42 commits intomainfrom
feature/custom-dns-zones
Jan 16, 2026
Merged

[management] Add custom dns zones#4849
bcmmbaga merged 42 commits intomainfrom
feature/custom-dns-zones

Conversation

@bcmmbaga
Copy link
Copy Markdown
Contributor

@bcmmbaga bcmmbaga commented Nov 24, 2025

Describe your changes

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

netbirdio/docs#518

Summary by CodeRabbit

  • New Features

    • DNS Zones CRUD and DNS Records (A/AAAA/CNAME) via REST and client API; server persistence and events.
    • Per-account DNS zones integrated into peer network maps with per-peer access filtering and search-domain controls.
    • Activity events for zone/record lifecycle and background peer updates.
  • Bug Fixes

    • Validation to prevent DNS domain conflicts between account settings and custom zones.
  • Tests

    • Extensive unit/integration tests for zones, records, storage, client, and network-map behavior.
  • Documentation

    • OpenAPI and generated API types updated for DNS Zones and DNS Records.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 24, 2025

📝 Walkthrough

Walkthrough

Adds first-class DNS zones and records across models, store, managers, HTTP API, client, tests, and activity codes; threads per-account zones into network-map assembly with per-peer access filtering and updates many function signatures to propagate zone-related parameters (peersCustomZone, accountZones).

Changes

Cohort / File(s) Summary
Zones: core types & API
management/internals/modules/zones/zone.go, management/internals/modules/zones/interface.go, management/internals/modules/zones/manager/api.go
New Zone model, zones.Manager interface, and HTTP CRUD handlers (auth, validation, request/response).
Zones: manager impl & tests
management/internals/modules/zones/manager/manager.go, management/internals/modules/zones/manager/manager_test.go
Concrete manager implementing CRUD with transactions, domain conflict checks, events, peer-update triggers, and unit tests.
DNS Records: core & API
management/internals/modules/zones/records/record.go, management/internals/modules/zones/records/interface.go, management/internals/modules/zones/records/manager/api.go
New Record type (A/AAAA/CNAME) with validation, records.Manager interface, and HTTP CRUD handlers.
DNS Records: manager impl & tests
management/internals/modules/zones/records/manager/manager.go, management/internals/modules/zones/records/manager/manager_test.go
Records manager with conflict detection, transactional mutations, events, and comprehensive tests.
Store API & SQL impl + tests
management/server/store/store.go, management/server/store/sql_store.go, management/server/store/sql_store_test.go
Store interface extended for zones/records; SQL store CRUD/query implementations, AutoMigrate additions, and store tests.
Network-map controller & repo plumbing
management/internals/controllers/network_map/controller/controller.go, management/internals/controllers/network_map/controller/repository.go
Repository adds GetAccountZones; controller threads peersCustomZone and accountZones through network-map generation and propagates errors.
Network-map builder & account types
management/server/types/networkmapbuilder.go, management/server/types/networkmap.go, management/server/types/account.go, management/server/types/networkmap_golden_test.go, management/server/types/account_test.go
Builder and Account APIs extended to accept accountZones; assembleNetworkMap/GetPeerNetworkMap updated to merge peer custom zones with filtered account zones via filterPeerAppliedZones. Tests adjusted.
Account settings validation
management/server/account.go, management/server/account_test.go
validateSettingsUpdate gains transaction param and checks DNSDomain conflicts against existing zones; test added for DNS domain conflict.
Server wiring & HTTP handler
management/internals/server/boot.go, management/internals/server/modules.go, management/server/http/handler.go, management/server/http/testing/testing_tools/channel/channel.go
Added ZonesManager and RecordsManager factories; NewAPIHandler signature extended and test wiring updated to pass new managers.
Peers HTTP handler adjustments
management/server/http/handlers/peers/peers_handler.go
Per-peer custom-zone handling simplified (passes empty/nil into network-map generation).
gRPC conversion & util
management/internals/shared/grpc/conversion.go, management/server/util/util.go
Proto conversion includes SearchDomainDisabled; adds IsValidDomain regex helper used by zone/record validation.
Activity codes & errors
management/server/activity/codes.go, shared/management/status/error.go
New activity codes for DNS zone/record events and NotFound helpers for zone/record.
REST client & tests
shared/management/client/rest/client.go, shared/management/client/rest/dns_zones.go, shared/management/client/rest/dns_zones_test.go
Adds DNSZones REST client for zones and records, initializes client field, and adds integration-style tests.
OpenAPI & generated types
shared/management/http/api/openapi.yml, shared/management/http/api/types.gen.go
OpenAPI schemas and generated types for Zone, ZoneRequest, DNSRecord, DNSRecordRequest, DNSRecordType; new DNS zones endpoints.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant HTTP_API as "HTTP API"
    participant ZonesMgr as "ZonesManager"
    participant Store
    participant AccountMgr as "AccountManager"

    Note over Client,HTTP_API: Create DNS Zone
    Client->>HTTP_API: POST /api/dns/zones (ZoneRequest)
    HTTP_API->>ZonesMgr: CreateZone(ctx, accountID, userID, zone)
    ZonesMgr->>Store: GetZoneByDomain(ctx,...)
    alt domain exists
        ZonesMgr-->>HTTP_API: 400 Conflict
        HTTP_API-->>Client: 400
    else
        ZonesMgr->>Store: BeginTx / CreateZone(zone)
        ZonesMgr->>Store: Commit
        ZonesMgr->>AccountMgr: StoreEvent(DNSZoneCreated)
        ZonesMgr->>AccountMgr: Trigger background peer updates (async)
        ZonesMgr-->>HTTP_API: 201 Created (Zone)
        HTTP_API-->>Client: 201
    end
Loading
sequenceDiagram
    participant Requester
    participant NetworkMapSvc as "NetworkMapController"
    participant Store
    participant Account as "Account"
    participant Builder as "NetworkMapBuilder"

    Note over Requester,NetworkMapSvc: Peer requests network map
    Requester->>NetworkMapSvc: GetPeerNetworkMap(peerID, peersCustomZone)
    NetworkMapSvc->>Store: GetAccountZones(accountID)
    NetworkMapSvc->>Account: GetPeerGroups(peerID)
    NetworkMapSvc->>Builder: GetPeerNetworkMap(peerID, peersCustomZone, accountZones, validatedPeers)
    Builder->>Account: filterPeerAppliedZones(ctx, accountZones, peerGroups)
    Builder->>Builder: assembleNetworkMap(... merged zones ...)
    Builder-->>NetworkMapSvc: NetworkMap
    NetworkMapSvc-->>Requester: NetworkMap
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • lixmal
  • pappz
  • pascal-fischer

Poem

🐰 I dug a burrow full of zones and names,
I hopped past records — A, AAAA, and CNAMEs,
I nudged the store, then rang events with glee,
Peers fetched new maps and danced to DNS harmony,
Hooray — the warren’s zones are now set free!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description follows the template structure but leaves critical sections empty: 'Describe your changes', 'Issue ticket number and link', and 'Stack' are all blank. Only the documentation section and checklist are completed. Fill in the 'Describe your changes' section with details about the DNS zones feature, provide the related issue ticket number/link, and complete the 'Stack' section if applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature added: custom DNS zones management. It is specific, concise, and directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c9fef8 and d2aa02f.

📒 Files selected for processing (1)
  • client/internal/dns/server_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/internal/dns/server_test.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Client / Unit
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: release_ui
  • GitHub Check: FreeBSD Port / Build & Test
  • GitHub Check: release_ui_darwin
  • GitHub Check: Client / Unit
  • GitHub Check: release
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: JS / Lint
  • GitHub Check: Build Cache

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
@bcmmbaga bcmmbaga marked this pull request as ready for review November 28, 2025 09:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
management/server/types/account.go (1)

269-279: Based on my thorough verification of the codebase, I can now provide the final assessment:

Both issues in the review comment are confirmed and valid

The code inspection reveals:

  1. Flag dropping is confirmed: Both Account.GetPeerNetworkMap and NetworkMapBuilder.assembleNetworkMap create a new CustomZone with only Domain and Records fields:

    customZones[i] = nbdns.CustomZone{
        Domain:  customZone.Domain,
        Records: records,
    }

    This leaves SearchDomainDisabled and SkipPTRProcess zero-initialized (false), dropping the original values.

  2. In-place mutation is confirmed: The input customZones slice is modified directly in-place. While current call patterns typically pass fresh or empty slices, the function signature permits slice reuse, and any such reuse would result in cumulative filtering across multiple peers.

The CustomZone struct in dns/dns.go definitively has all four fields (Domain, Records, SearchDomainDisabled, SkipPTRProcess), confirming the impact of this code pattern.


Preserve CustomZone flags and avoid surprising slice mutation in GetPeerNetworkMap

Two issues in this block:

  1. Flags are dropped for the account DNS zone

    When a zone matches dns.Fqdn(dnsDomain), the code replaces the entire element:

    customZones[i] = nbdns.CustomZone{
        Domain:  customZone.Domain,
        Records: records,
    }

    This zeroes SearchDomainDisabled and SkipPTRProcess, changing resolver behavior for that zone.

    At minimum, keep the original flags:

  •   records := filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
    
  •   customZones[i] = nbdns.CustomZone{
    
  •       Domain:  customZone.Domain,
    
  •       Records: records,
    
  •   }
    
  •   records := filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
    
  •   customZones[i].Records = records
    
    
    
  1. In‑place mutation can corrupt subsequent calls

    GetPeerNetworkMap mutates the passed customZones slice. If callers reuse a single slice for multiple peers, records are progressively intersected (peer 2 sees only a subset derived from peer 1's view).

    Consider copying before filtering to make the function side‑effect‑free for callers:

  • if dnsManagementStatus {
  •   for i, customZone := range customZones {
    
  • if dnsManagementStatus {
  •   filteredZones := slices.Clone(customZones)
    
  •   for i, customZone := range filteredZones {
           if customZone.Domain == dns.Fqdn(dnsDomain) {
    
  •           records := filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
    
  •           customZones[i].Records = records
    
  •           filteredZones[i].Records = filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
           }
    
  •   }
    
  •   dnsUpdate.CustomZones = customZones
    
  •   }
    
  •   dnsUpdate.CustomZones = filteredZones
    
    
    

Also applies to: NetworkMapBuilder.assembleNetworkMap

🧹 Nitpick comments (8)
management/server/util/util.go (1)

3-6: Domain validation helper is fine; just confirm label policy

IsValidDomain and the compiled domainRegex give a clear, centralized domain check and cover common cases (including *.example.com).

Be aware of two behavioral details:

  • Labels with underscores (e.g. _acme-challenge.example.com) are rejected.
  • Labels starting/ending with - are accepted, even though they’re not valid hostnames per RFCs.

If that matches your product’s intended constraints, this is good to go; otherwise you may want to tighten/relax the regex accordingly.

Also applies to: 58-63

management/server/http/handlers/peers/peers_handler.go (1)

13-14: Avoid unnecessary dns.CustomZone dependency in handler

GetAccessiblePeers doesn’t consume CustomZones from the returned network map, so you can avoid importing github.com/netbirdio/netbird/dns and allocating an empty slice. Passing nil is simpler and keeps DNS concerns encapsulated deeper in the stack.

-import (
+import (
   ...
-  "github.com/netbirdio/netbird/dns"
   ...
 )
 ...
- netMap := account.GetPeerNetworkMap(r.Context(), peerID, dnsDomain, []dns.CustomZone{}, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
+ netMap := account.GetPeerNetworkMap(r.Context(), peerID, dnsDomain, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)

Also applies to: 324-327

shared/management/http/api/openapi.yml (2)

1765-1786: Clarify whether DNS record name is FQDN or zone‑relative

DNSRecordRequest.name is documented with example "www", which suggests a label relative to the zone:

    DNSRecordRequest:
      properties:
        name:
          description: DNS record name
          example: www

However, the server‑side validation in management/internals/modules/zones/records/manager/manager.go currently requires record.Name to HasSuffix(zone.Domain), and the integration test creates records with full FQDNs like "api.test.example.com" for a zone domain "test.example.com".

This mismatch can confuse API consumers.

Suggestion

Either:

  • Keep the current server behavior (full FQDN) and update the description/example to something like "www.example.com" / "api.test.example.com", or
  • Change server validation to accept zone‑relative labels and append the zone domain internally.

1816-1834: Extend activity_code enum with new DNS zone/record activities

The Event.activity_code enum doesn’t list the new DNS activities (dns.zone.create/update/delete, dns.zone.record.create/update/delete) you introduced in management/server/activity/codes.go. As a result, documented values are out of sync with what the backend can emit.

Consider appending these new codes to the enum so API clients see the full set of possible activity_code values.

management/internals/modules/zones/interface.go (1)

7-12: Zone Manager interface is fine; consider tightening GetZone naming

The CRUD interface here is consistent with the Zone model and store APIs. For readability and consistency with DeleteZone(..., zoneID string), consider renaming the last parameter of GetZone from zone to zoneID in implementations and usages.

management/internals/modules/zones/zone.go (1)

60-77: Consider adding a maximum length check for the domain field.

While util.IsValidDomain validates the format, DNS domains have a maximum length of 253 characters. Consider adding a length check similar to the name field validation for defense in depth.

 	if !util.IsValidDomain(z.Domain) {
 		return errors.New("invalid zone domain format")
 	}
+
+	if len(z.Domain) > 253 {
+		return errors.New("zone domain exceeds maximum length of 253 characters")
+	}
management/server/types/networkmapbuilder.go (1)

13-13: Normalize domains consistently and avoid dropping CustomZone fields

The new DNS zone filtering logic is sound, but two details are worth tightening up:

  • The comparison customZone.Domain == dns.Fqdn(dnsDomain) will only match if customZone.Domain is already in fully‑qualified form (trailing dot). If Zone.Domain is stored without a trailing dot, this will silently skip zones. Consider normalizing both sides (e.g., dns.Fqdn on both or a shared helper) to avoid mismatches.

  • When you rebuild a matching zone as nbdns.CustomZone{Domain: customZone.Domain, Records: records}, any other fields on nbdns.CustomZone are zeroed out. To future‑proof this, it’s safer to copy the existing struct and only override Records (e.g., z := customZone; z.Records = records; customZones[i] = z).

Also applies to: 939-942, 978-982, 1028-1040

management/server/store/sql_store.go (1)

113-119: Zone/DNS record store methods mostly align; consider association scope and error detail

The new zone/DNS‑record CRUD methods look consistent with existing store patterns, but a couple of refinements would make them safer and easier to debug:

  • UpdateZone (and to a lesser extent UpdateDNSRecord) uses Select("*").Save(zone). Given zones.Zone declares Records []*records.Record \gorm:"foreignKey:ZoneID;references:ID"`inmanagement/internals/modules/zones/zone.golines 12–21, this can implicitly upsert associated records when callers only intend to update zone metadata. If records are supposed to be managed exclusively via the DNS record methods, it’s safer toOmit(clause.Associations)or explicitly select just the zone fields to avoid accidental writes toRecords`.

  • The new log messages are quite generic (e.g., "failed to create to store" / "failed to delete dns record from store"). Including the relevant IDs (accountID, zoneID, recordID) in the log and/or wrapping error text will make production debugging a lot easier.

These are behavioural/observability tweaks; the core implementation and account scoping look correct.

Also applies to: 4131-4294

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aca0398 and 0e644d2.

📒 Files selected for processing (31)
  • management/internals/controllers/network_map/controller/controller.go (13 hunks)
  • management/internals/controllers/network_map/controller/repository.go (3 hunks)
  • management/internals/modules/zones/interface.go (1 hunks)
  • management/internals/modules/zones/manager/api.go (1 hunks)
  • management/internals/modules/zones/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/interface.go (1 hunks)
  • management/internals/modules/zones/records/manager/api.go (1 hunks)
  • management/internals/modules/zones/records/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/record.go (1 hunks)
  • management/internals/modules/zones/zone.go (1 hunks)
  • management/internals/server/boot.go (1 hunks)
  • management/internals/server/modules.go (2 hunks)
  • management/internals/shared/grpc/conversion.go (1 hunks)
  • management/server/account_test.go (1 hunks)
  • management/server/activity/codes.go (2 hunks)
  • management/server/http/handler.go (3 hunks)
  • management/server/http/handlers/peers/peers_handler.go (2 hunks)
  • management/server/http/testing/testing_tools/channel/channel.go (2 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/store/store.go (2 hunks)
  • management/server/types/account.go (2 hunks)
  • management/server/types/networkmap.go (1 hunks)
  • management/server/types/networkmap_golden_test.go (18 hunks)
  • management/server/types/networkmapbuilder.go (5 hunks)
  • management/server/util/util.go (2 hunks)
  • shared/management/client/rest/client.go (2 hunks)
  • shared/management/client/rest/dns_zones.go (1 hunks)
  • shared/management/client/rest/dns_zones_test.go (1 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
  • shared/management/http/api/types.gen.go (4 hunks)
  • shared/management/status/error.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
shared/management/client/rest/client.go (2)
shared/management/client/rest/dns.go (1)
  • DNSAPI (12-14)
shared/management/client/rest/dns_zones.go (1)
  • DNSZonesAPI (12-14)
management/internals/modules/zones/records/interface.go (1)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/internals/modules/zones/interface.go (2)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1928)
management/server/http/handler.go (4)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/modules/zones/records/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/shared/grpc/conversion.go (1)
shared/management/proto/management.pb.go (3)
  • SimpleRecord (2752-2762)
  • SimpleRecord (2777-2777)
  • SimpleRecord (2792-2794)
management/server/types/networkmap.go (2)
management/server/telemetry/accountmanager_metrics.go (1)
  • AccountManagerMetrics (11-17)
management/server/types/networkmapbuilder.go (1)
  • NetworkMapCache (33-59)
management/internals/modules/zones/records/manager/manager.go (9)
management/server/store/store.go (1)
  • Store (52-222)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (23-29)
management/internals/modules/zones/records/record.go (3)
  • Record (21-29)
  • NewRecord (31-41)
  • RecordTypeCNAME (18-18)
management/server/permissions/modules/module.go (1)
  • Dns (11-11)
management/server/permissions/operations/operation.go (2)
  • Read (7-7)
  • Delete (9-9)
shared/management/status/error.go (6)
  • NewPermissionValidationError (213-215)
  • NewPermissionDeniedError (209-211)
  • Type (46-46)
  • Errorf (70-75)
  • InvalidArgument (27-27)
  • AlreadyExists (30-30)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
route/route.go (1)
  • ID (48-48)
management/server/types/account.go (2)
dns/dns.go (1)
  • CustomZone (43-52)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
management/internals/server/boot.go (1)
management/server/http/handler.go (1)
  • NewAPIHandler (57-145)
shared/management/client/rest/dns_zones_test.go (4)
shared/management/http/api/types.gen.go (8)
  • DNSRecord (418-433)
  • DNSRecordTypeA (17-17)
  • PostApiDnsZonesJSONRequestBody (2027-2027)
  • PutApiDnsZonesZoneIdJSONRequestBody (2030-2030)
  • PostApiDnsZonesZoneIdRecordsJSONRequestBody (2033-2033)
  • PutApiDnsZonesZoneIdRecordsRecordIdJSONRequestBody (2036-2036)
  • ZoneRequest (1931-1946)
  • DNSRecordRequest (436-448)
shared/management/http/util/util.go (1)
  • ErrorResponse (21-24)
management/server/activity/codes.go (1)
  • Code (9-12)
shared/management/status/error.go (1)
  • Error (54-57)
management/internals/controllers/network_map/controller/repository.go (1)
management/server/store/store.go (1)
  • LockingStrengthNone (49-49)
management/internals/modules/zones/records/record.go (2)
shared/management/http/api/types.gen.go (3)
  • DNSRecord (418-433)
  • DNSRecordType (451-451)
  • DNSRecordRequest (436-448)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
management/internals/controllers/network_map/controller/controller.go (4)
management/internals/controllers/network_map/interface.go (1)
  • Controller (23-39)
management/server/types/account.go (1)
  • LookupMap (57-57)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
management/server/store/store.go (3)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1928)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/server/account_test.go (2)
dns/dns.go (1)
  • CustomZone (43-52)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
management/server/store/sql_store.go (4)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (4)
  • Error (54-57)
  • Errorf (70-75)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
management/server/store/store.go (2)
  • LockingStrength (42-42)
  • LockingStrengthNone (49-49)
management/internals/modules/zones/zone.go (3)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/http/api/types.gen.go (2)
  • Zone (1910-1928)
  • ZoneRequest (1931-1946)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
🔇 Additional comments (47)
management/internals/modules/zones/manager/api.go (1)

16-160: Zones HTTP handlers follow existing patterns cleanly

Auth extraction, validation, manager delegation, and JSON responses look consistent and correct; no issues spotted in routing or parameter handling.

management/internals/shared/grpc/conversion.go (1)

329-345: gRPC custom zone conversion correctly propagates SearchDomainDisabled

The added SearchDomainDisabled field is wired through alongside Domain and preallocated Records; conversion logic remains consistent.

management/server/http/testing/testing_tools/channel/channel.go (1)

12-15: Test HTTP channel correctly wires zones and records managers

Using real zonesManager.NewManager and recordsManager.NewManager with the test store and passing them into NewAPIHandler keeps the black-box tests exercising the new DNS zones/records API surface as expected.

Also applies to: 92-101

management/internals/server/modules.go (1)

9-12: Zones/records manager accessors align with existing server module patterns

ZonesManager() and RecordsManager() follow the same lazy-initialization pattern as other managers and use the expected dependencies (Store, AccountManager, PermissionsManager), so the wiring looks correct.

Also applies to: 123-133

management/server/types/networkmap.go (1)

24-34: Network map API cleanly extended for multi‑zone DNS

Extending GetPeerNetworkMapExp with dnsDomain and customZones and forwarding them to NetworkMapCache.GetPeerNetworkMap is a minimal, coherent change; cache initialization semantics remain unchanged.

shared/management/client/rest/client.go (1)

60-67: REST client DNSZones API wiring is consistent and complete

Adding the DNSZones field and initializing it in initialize() mirrors the existing DNS API pattern and should make the new endpoints immediately usable via the client.

Also applies to: 107-121

shared/management/status/error.go (1)

256-264: LGTM!

The new error constructors follow the established pattern in this file and are consistent with other NotFound error types.

management/server/http/handler.go (3)

17-20: LGTM!

Import aliases are appropriately named to avoid conflicts with the interface types.


72-73: LGTM!

New manager parameters follow the established injection pattern.


141-142: LGTM!

Endpoint registration follows the established pattern and maintains logical grouping with DNS-related handlers.

management/internals/controllers/network_map/controller/repository.go (1)

6-6: LGTM!

The new GetAccountZones method follows the established repository pattern, using LockingStrengthNone consistent with other read methods in this file.

Also applies to: 16-16, 42-45

shared/management/http/api/types.gen.go (1)

1-4: Generated code - no manual changes required.

This file is auto-generated by oapi-codegen. The new DNS zone and record types appear correctly structured with appropriate field types and optionality for request/response patterns.

management/server/account_test.go (1)

397-399: Test correctly adapts to updated GetPeerNetworkMap signature with domain and zones slice parameters.

The test at lines 397-399 properly implements the new signature:

  • Passes "netbird.io" as the dnsDomain string parameter
  • Wraps customZone in []nbdns.CustomZone{customZone} slice to support multiple zones
  • All other parameters (context, peerID, validatedPeers, policies, routers, metrics) match the function signature

The implementation aligns with the underlying function that iterates over the zones slice to filter records per domain.

management/internals/server/boot.go (1)

96-96: Perfect! All verifications are complete. The signature shows:

  • Parameter 15: zManager zones.Manager
  • Parameter 16: rManager records.Manager
  • Parameter 17: networkMapController network_map.Controller

These match the ZonesManager() and RecordsManager() methods that return zones.Manager and records.Manager respectively. The code change is correctly implemented.


LGTM!

Both ZonesManager() and RecordsManager() methods exist on BaseServer and return the correct types (zones.Manager and records.Manager). The NewAPIHandler signature correctly accepts these parameters at positions 15-16, and they are properly passed from the call site.

management/server/activity/codes.go (1)

183-189: DNS zone/record activity codes and mappings look consistent

New DNS activity constants and their activityMap entries follow the existing naming and coding conventions; no functional issues spotted here.

Also applies to: 300-306

management/internals/modules/zones/records/interface.go (1)

7-13: Records Manager interface shape is reasonable

The CRUD interface is coherent (context-first, explicit account/user/zone IDs) and matches the underlying Record model and store APIs.

management/server/types/account.go (1)

1755-1778: Per‑peer DNS record filtering helper looks correct

filterZoneRecordsForPeers builds a set of IPs for the target peer, its reachable peers, and expired peers, and filters zone records by RData membership. The logic is straightforward and matches the goal of scoping DNS responses to the peer’s connectivity set.

shared/management/http/api/openapi.yml (1)

1710-1756: Zone schemas look consistent with generated types

ZoneRequest / Zone schemas (fields, required sets, and examples) line up with the generated api.Zone / api.ZoneRequest structs and the domain model (zones.Zone). The use of Enabled *bool in the request and required enabled in the response is a sensible pattern for create/update semantics.

management/internals/modules/zones/records/manager/manager.go (1)

33-55: Permissions + transactional flow for DNS records look solid

  • Permission checks for read/create/update/delete use the DNS module and appropriate operations and map errors via status.NewPermissionValidationError / NewPermissionDeniedError.
  • All mutating operations run inside ExecuteInTransaction, lock the zone (LockingStrengthUpdate), update the record, and increment the network serial before emitting events.

This pattern matches the rest of the codebase’s account/DNS workflows.

Also applies to: 101-205

shared/management/client/rest/dns_zones_test.go (3)

40-213: Mocked DNS zones client tests provide good coverage

The mocked tests for zone CRUD operations verify HTTP methods, request body shapes (via unmarshalling), and error handling for 4xx responses. This should catch most regressions in the REST client layer.


215-390: Mocked DNS record client tests are comprehensive

Similarly, record CRUD tests validate correct endpoints, methods, and JSON payloads, plus propagate server error responses into Go errors, which is exactly what the client should be doing.


392-460: End‑to‑end integration test nicely validates the full DNS zones/records flow

TestDNSZones_Integration walks through create/list/get/update/delete for both zones and records against a black‑box server, asserting realistic values (full FQDN for record name, distribution groups, etc.). This is a valuable high‑level guardrail for the new feature.

management/internals/modules/zones/records/manager/api.go (6)

16-30: LGTM!

The handler struct and route registration are well-structured. The RESTful endpoints follow proper conventions for DNS record CRUD operations.


32-57: LGTM!

The getAllRecords handler correctly validates authentication, zone ID, and properly converts internal records to API responses.


59-93: LGTM!

The createRecord handler follows the standard pattern: decode request, convert to domain model, validate, and delegate to manager. The error handling is consistent.


95-121: LGTM!

The getRecord handler correctly validates both zone and record IDs before delegating to the manager.


123-164: LGTM!

The updateRecord handler correctly uses the path parameter for the record ID, ensuring the URL is the source of truth for the resource identifier.


166-191: LGTM!

The deleteRecord handler follows proper REST conventions, returning an empty object on successful deletion.

management/internals/modules/zones/zone.go (5)

13-22: LGTM!

The Zone struct is well-defined with appropriate GORM tags. The JSON serializer for DistributionGroups and the foreign key relationship for Records are correctly configured.


24-34: LGTM!

The constructor properly initializes all fields and generates a unique ID using xid.


36-45: LGTM!

The API response mapping is complete and correctly maps all Zone fields to the API representation.


47-58: LGTM!

The FromAPIRequest method correctly handles the optional Enabled field by defaulting to true when not provided, which is a sensible default for new zones.


79-81: LGTM!

The EventMeta method provides appropriate metadata for event tracking.

management/internals/controllers/network_map/controller/controller.go (6)

174-178: LGTM!

Proper error handling for zone retrieval with logging and early return on failure.


831-888: LGTM!

The filterPeerAppliedZones function correctly:

  • Filters zones by peer group membership
  • Skips disabled zones and zones without records
  • Maps record types to DNS type constants
  • Logs warnings for unknown record types instead of failing
  • Properly inverts EnableSearchDomain to SearchDomainDisabled

320-328: LGTM!

The zone retrieval and filtering logic in UpdateAccountPeer is consistent with the pattern used in other methods.


459-470: LGTM!

Zone retrieval and filtering follows the established pattern in the controller.


789-800: LGTM!

The zone handling in GetNetworkMap is consistent with other controller methods.


500-518: LGTM!

The signature update to accept customZones []nbdns.CustomZone properly supports the multi-zone architecture.

management/server/types/networkmap_golden_test.go (2)

107-108: LGTM!

The test signature updates are consistent across all test functions and benchmarks.


72-72: Golden tests intentionally use empty zones; CustomZone functionality is tested separately.

The golden tests pass empty []dns.CustomZone{} by design—they validate baseline network map structure rather than DNS zone filtering. Comprehensive CustomZone testing already exists in management/server/account_test.go (line 398), where GetPeersCustomZone() is called with actual zone data, and in management/server/types/account_test.go (lines 1119–1209), which covers zone record filtering scenarios. No additional coverage is needed in these golden tests.

management/internals/modules/zones/records/record.go (5)

13-29: LGTM!

The RecordType constants and Record struct are well-defined with appropriate GORM tags for indexing and primary key.


31-41: LGTM!

The constructor properly initializes all fields and generates a unique ID.


43-59: LGTM!

The API conversion methods correctly map between internal and external record representations.


61-96: LGTM!

The validation logic is comprehensive:

  • Name format validation using domain regex
  • Type-specific content validation (IPv4, IPv6, domain)
  • Non-negative TTL check
  • Clear error messages for each validation failure

98-107: LGTM!

The EventMeta method appropriately includes both record details and zone context for comprehensive event tracking.

management/server/store/store.go (1)

26-28: DNS zone and DNS record methods on Store look consistent

The new zone and DNS record operations on Store follow existing patterns (account‑scoped IDs, read methods with LockingStrength) and match the SqlStore implementations. No issues from the interface design side.

Also applies to: 209-221

Comment thread management/internals/controllers/network_map/controller/controller.go Outdated
Comment thread management/internals/modules/zones/manager/manager.go
Comment thread management/internals/modules/zones/manager/manager.go
Comment thread management/internals/modules/zones/records/manager/manager.go
Comment thread management/internals/modules/zones/records/manager/manager.go
Comment thread management/internals/modules/zones/records/record.go
Comment thread shared/management/client/rest/dns_zones.go
Comment thread shared/management/http/api/openapi.yml
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
management/internals/modules/zones/manager/manager.go (1)

167-172: Fix closure capture of loop variable record.

This issue was flagged in a previous review and remains unfixed. The closures built inside the loop capture record by reference, so when they execute (lines 184-186), all of them will reference the last record from the loop. This causes incorrect event IDs and metadata for all deleted records except the last one.

Apply this diff to capture the current record value in each iteration:

-	for _, record := range records {
-		eventsToStore = append(eventsToStore, func() {
-			meta := record.EventMeta(zone.ID, zone.Name)
-			m.accountManager.StoreEvent(ctx, userID, record.ID, accountID, activity.DNSRecordDeleted, meta)
-		})
-	}
+	for _, record := range records {
+		rec := record
+		eventsToStore = append(eventsToStore, func() {
+			meta := rec.EventMeta(zone.ID, zone.Name)
+			m.accountManager.StoreEvent(ctx, userID, rec.ID, accountID, activity.DNSRecordDeleted, meta)
+		})
+	}
🧹 Nitpick comments (2)
management/server/store/sql_store_test.go (1)

3859-3879: Consider testing cascading delete behavior.

While the current tests verify zone deletion works correctly, it would be helpful to optionally add a test that verifies whether DNS records are automatically deleted when their parent zone is deleted (if cascading delete is implemented), or if they need to be deleted separately.

Example test structure:

func TestSqlStore_DeleteZoneCascade(t *testing.T) {
	// Create zone
	// Create DNS records in zone
	// Delete zone
	// Verify records are also deleted (or handle accordingly)
}
management/internals/modules/zones/manager/manager.go (1)

188-188: Consider using a detached context for background peer updates.

The goroutine receives the request context ctx, which may be canceled when the HTTP request completes. If UpdateAccountPeers is a long-running operation, it could be interrupted prematurely.

If peer updates should complete regardless of client disconnection, consider using a detached context:

go m.accountManager.UpdateAccountPeers(context.Background(), accountID)

However, if cancellation is intentional (e.g., to respect request timeouts), the current implementation is fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e644d2 and 25c9cb2.

📒 Files selected for processing (7)
  • management/internals/controllers/network_map/controller/controller.go (13 hunks)
  • management/internals/controllers/network_map/controller/controller_test.go (2 hunks)
  • management/internals/modules/zones/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/record.go (1 hunks)
  • management/server/store/sql_store_test.go (2 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
management/internals/modules/zones/records/record.go (2)
shared/management/http/api/types.gen.go (3)
  • DNSRecord (418-433)
  • DNSRecordType (451-451)
  • DNSRecordRequest (436-448)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
management/internals/modules/zones/records/manager/manager.go (8)
management/server/store/store.go (1)
  • Store (52-222)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (23-29)
management/internals/modules/zones/records/record.go (3)
  • Record (21-29)
  • NewRecord (31-41)
  • RecordTypeCNAME (18-18)
management/server/permissions/modules/module.go (1)
  • Dns (11-11)
management/server/permissions/operations/operation.go (2)
  • Read (7-7)
  • Delete (9-9)
shared/management/status/error.go (4)
  • NewPermissionValidationError (213-215)
  • NewPermissionDeniedError (209-211)
  • Type (46-46)
  • Errorf (70-75)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: release_ui_darwin
  • GitHub Check: release
  • GitHub Check: release_ui
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: Build Cache
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
🔇 Additional comments (33)
management/internals/modules/zones/records/manager/manager.go (6)

19-31: LGTM!

The manager implementation follows the established pattern with proper dependency injection of store, account manager, and permissions manager.


33-55: LGTM!

Read operations properly implement permission checks and use appropriate locking strength for concurrent reads.


57-101: LGTM!

The previous concern about missing peer updates has been resolved. The method now correctly triggers UpdateAccountPeers at line 98, ensuring new DNS records are propagated to peers proactively, consistent with the update and delete paths.


103-160: LGTM!

The update logic correctly distinguishes between changes that require conflict revalidation (Name, Type, Content) and those that don't (TTL). The implementation properly updates all fields while only triggering expensive conflict checks when necessary.


162-207: LGTM!

Delete operation correctly implemented with proper locking, transactional safety, event logging, and peer updates.


209-236: LGTM!

The zone membership validation has been corrected to properly check label boundaries (line 211), addressing the previous review concern. The implementation now correctly accepts only example.com or *.example.com for zone example.com, while rejecting notexample.com.

The duplicate and CNAME conflict checks are also correctly implemented.

shared/management/http/api/openapi.yml (3)

1710-1801: LGTM!

The previous issue with DNSRecord requiring an undefined enabled field has been resolved. The schema now correctly requires only id (line 1800) and inherits the necessary fields from DNSRecordRequest. All DNS Zone and Record schemas are well-defined and consistent with the backend implementation.


4592-4741: LGTM!

All DNS Zone CRUD endpoints are properly defined with:

  • Appropriate HTTP methods and paths
  • Correct security requirements
  • Proper request/response schemas
  • Comprehensive error response codes

The API design follows RESTful conventions consistently.


4742-4932: LGTM!

DNS Record endpoints are well-structured as nested resources under zones (/api/dns/zones/{zoneId}/records). All endpoints properly:

  • Define required path parameters (zoneId, recordId)
  • Use correct request/response schemas
  • Include appropriate error codes (notably 404 for resource-not-found scenarios)
  • Follow consistent patterns with the parent zone endpoints
management/server/store/sql_store_test.go (3)

25-26: LGTM! New imports are correctly added for zone and DNS record testing.

The imports properly reference the new internal modules for zones and DNS records that align with the test coverage being added.


3723-3879: Excellent test coverage for zone CRUD operations.

The zone tests comprehensively cover creation, retrieval, update, and deletion with proper error handling and NotFound verification after deletion. The test structure follows existing patterns in the file and uses appropriate assertions.


3881-4126: Comprehensive DNS record test coverage with excellent structure.

The DNS record tests thoroughly cover CRUD operations including:

  • Creation with proper zone dependencies
  • Retrieval by ID and by name
  • Updates to all record fields
  • Individual and bulk deletion
  • Multiple record types (A, AAAA, CNAME)

All tests include proper error handling and NotFound verification.

management/internals/modules/zones/records/record.go (6)

13-29: Well-structured Record model with proper GORM configuration.

The RecordType constants provide type safety for the three supported DNS record types, and the Record struct includes appropriate GORM tags for database indexing and primary key designation.


31-41: LGTM! Constructor properly initializes Record with unique ID.

The use of xid for ID generation ensures uniqueness, and all fields are properly initialized.


43-59: API conversion methods correctly map between internal and external types.

Both ToAPIResponse and FromAPIRequest properly convert between the internal Record model and the API types, with correct field mappings.


61-96: Comprehensive validation with appropriate type-specific checks.

The validation logic properly enforces:

  • Non-empty name with domain format validation
  • Type-specific content validation (IPv4 for A, IPv6 for AAAA, domain for CNAME)
  • Non-negative TTL

Note: The TTL validation allows 0, which is valid in DNS for indicating no caching should occur.


120-129: IPv6 validation fix has been correctly applied.

The validateIPv6 function now properly rejects IPv4 addresses by checking ip.To4() != nil. This correctly addresses the critical bug from the previous review where ip.To16() == nil would incorrectly accept IPv4 addresses (since To16() returns a 16-byte IPv4-mapped representation for IPv4 addresses).

The current implementation ensures only true IPv6 addresses are accepted for AAAA records.


98-118: EventMeta and validateIPv4 are correctly implemented.

The EventMeta method provides comprehensive metadata for event tracking, and validateIPv4 properly validates IPv4 addresses using the correct ip.To4() == nil check.

management/internals/controllers/network_map/controller/controller_test.go (2)

4-17: LGTM!

The new imports are appropriate for the test function and support comprehensive DNS zone testing.


120-630: Excellent test coverage!

The table-driven test comprehensively validates filterPeerAppliedZones across multiple scenarios:

  • Access control via distribution groups
  • Zone enablement filtering
  • Empty record handling
  • Multiple record types (A, AAAA, CNAME)
  • Search domain configuration
  • Multi-zone and multi-group scenarios

The assertions thoroughly verify both zone-level properties and individual record details.

management/internals/controllers/network_map/controller/controller.go (12)

15-15: LGTM!

The new imports support DNS zone filtering and record type mapping in the filterPeerAppliedZones function.

Also applies to: 23-24


174-178: LGTM!

Proper error handling for account zones retrieval with logging and early return.


203-205: LGTM! Bug from previous review has been fixed.

The code correctly uses p.ID (goroutine parameter) instead of the loop variable, avoiding the closure capture issue flagged in the previous review. The customZones construction properly filters zones by peer group membership and appends the peers custom zone.


210-213: LGTM!

Both experimental and non-experimental network map generation paths correctly updated to accept the customZones slice parameter.


320-327: LGTM!

Consistent zone retrieval and filtering pattern applied correctly.


332-335: LGTM!

Function calls correctly updated to pass customZones parameter in both code paths.


459-469: LGTM!

Proper sequence: retrieve account zones, get DNS domain and peers custom zone, then filter zones based on peer groups.


480-483: LGTM!

Function calls consistently updated with customZones parameter.


500-518: LGTM!

The getPeerNetworkMapExp function signature correctly updated to accept customZones slice, and properly retrieves DNS domain from account settings before passing to the network map builder.


790-799: LGTM!

Consistent zone retrieval and filtering pattern properly implemented in GetNetworkMap.


810-813: LGTM!

Network map generation calls correctly updated with customZones parameter.


831-888: Well-implemented helper function!

The filterPeerAppliedZones function correctly:

  • Filters zones based on peer group access control
  • Skips disabled zones and zones without records
  • Maps DNS record types (A, AAAA, CNAME) with proper fallback for unknown types
  • Constructs properly formatted FQDNs using dns.Fqdn()
  • Inverts EnableSearchDomain to SearchDomainDisabled correctly

The early return optimization and access control logic are both sound.

management/internals/modules/zones/manager/manager.go (1)

88-129: Verify if peer updates and serial increment are needed after zone updates.

UpdateZone correctly handles the transaction error (lines 122-124, resolving the previous review concern), but like CreateZone, it does not call IncrementNetworkSerial or UpdateAccountPeers. Changes to zone properties (enabled status, distribution groups, domain, search domain settings) likely affect the network map and should trigger peer updates.

Use the verification script from the CreateZone comment above to check if Update operations typically call these methods.

Comment thread management/internals/modules/zones/manager/manager.go
…ne management

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
shared/management/http/api/openapi.yml (3)

4594-4743: /api/dns/zones collection and item endpoints match existing REST patterns

  • List/create (GET/POST) endpoints for zones reuse the standard security blocks and response code conventions (200 + 400/401/403/500 on write).
  • Single-zone GET/PUT/DELETE correctly use zoneId as a path param with type string and add 404 for missing zones, which is actually stricter than some of the older DNS endpoints.

One minor consistency nit: existing DELETE endpoints often include an explicit empty content: { } block on 200, whereas DELETE /api/dns/zones/{zoneId} omits content. Both are valid OpenAPI, but you may want to align with the existing deletes for uniformity.


4744-4814: /api/dns/zones/{zoneId}/records collection endpoints are well-shaped

  • GET returns DNSRecord[] and includes 404 when the parent zone is missing.
  • POST accepts DNSRecordRequest and returns a full DNSRecord, consistent with other create endpoints returning the created resource.

Again, optional nit: you don’t expose a 400 for the GET despite other list endpoints sometimes defining it even without query params; that’s fine semantically, but if you aim for strict uniformity you might consider adding it later.


4816-4934: Record-level endpoints look correct; consider aligning delete response shape

  • GET/PUT for /api/dns/zones/{zoneId}/records/{recordId} use both zoneId and recordId with clear descriptions and return a single DNSRecord, including 404 for not found.
  • PUT reuses DNSRecordRequest for the body, which matches the Go types and keeps id server-controlled.

For DELETE, you return a bare 200 with description only, whereas most existing deletes specify content: { }. It’s valid as-is, but you might want to add the empty content block for consistency with the rest of the API surface.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1782bbe and 604085d.

📒 Files selected for processing (4)
  • management/internals/modules/zones/zone.go (1 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
  • shared/management/http/api/types.gen.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • shared/management/http/api/openapi.yml
  • shared/management/http/api/types.gen.go
  • management/server/store/sql_store.go
  • management/internals/modules/zones/zone.go
🧬 Code graph analysis (1)
management/internals/modules/zones/zone.go (3)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/http/api/types.gen.go (3)
  • Zone (1910-1931)
  • DNSRecord (418-433)
  • ZoneRequest (1934-1949)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
🔇 Additional comments (12)
management/internals/modules/zones/zone.go (1)

13-89: LGTM! Clean zone model implementation.

The Zone type is well-structured with appropriate GORM tags, proper foreign key relationships, sensible validation rules, and clean API conversion logic. The default handling for the optional Enabled field (defaulting to true when nil) aligns with typical feature flag patterns.

management/server/store/sql_store.go (4)

30-31: LGTM! Proper integration into auto-migration.

The imports and auto-migration additions correctly integrate the new zones and records models into the store initialization, ensuring database schema readiness.

Also applies to: 118-118


4141-4149: LGTM!

The use of Select("*").Save() for full updates is consistent with other entity update patterns in this store (e.g., SavePolicy).


4151-4199: LGTM! Standard CRUD patterns.

The zone retrieval and deletion methods follow established store conventions with proper locking support, association preloading, and error handling.


4235-4295: LGTM! DNS record retrieval methods are properly scoped.

The GetDNSRecordByID, GetZoneDNSRecords, GetZoneDNSRecordsByName, and DeleteZoneDNSRecords methods all correctly scope operations by both accountID and zoneID, ensuring proper data isolation.

shared/management/http/api/types.gen.go (5)

15-20: DNSRecordType constants correctly mirror OpenAPI enum

The A/AAAA/CNAME constants align with the DNSRecordType string enum in the OpenAPI spec; nothing to change here.


417-452: DNSRecord / DNSRecordRequest structs match schema expectations

DNSRecordRequest and DNSRecord fields (name, type, content, ttl, id) line up with the OpenAPI definitions and required sets; using a separate request type without id is consistent with other resources in this API.


450-451: String alias for DNSRecordType is appropriate

Defining DNSRecordType as string keeps the generated client simple while still being constrained by the enum constants above; this matches existing enum patterns in this file.


1909-1949: Zone / ZoneRequest shapes are consistent but rely on backend defaults for enabled

  • ZoneRequest.Enabled as *bool (omittable) vs Zone.Enabled as bool matches the OpenAPI idea of enabled being optional on write with a default of true, and required on read.
  • This puts the onus on handlers to interpret nil as “use default” and not as false.

Please double-check the zone create/update handlers treat nil Enabled correctly (defaulting to true on create and/or leaving it unchanged on update) so behavior matches the OpenAPI default.


2029-2040: Request body aliases for DNS zones/records follow existing patterns

The new PostApiDnsZones*/PutApiDnsZones* and records request-body aliases correctly point to ZoneRequest/DNSRecordRequest, matching the OpenAPI requestBody schemas and the conventions used elsewhere in this generated file.

shared/management/http/api/openapi.yml (2)

28-29: New “DNS Zones” tag integrates cleanly into tag list

Tag naming and description match the style of existing tags and will group the new endpoints coherently in generated docs.


1710-1803: Zone/DNSRecord schemas are structurally sound and consistent**

  • ZoneRequest and Zone follow the existing pattern of *Request vs full resource (allOf with an id-carrying object), similar to networks/resources/routes.
  • enabled being optional on ZoneRequest (with a default) and required via Zone’s required array is a valid allOf usage and matches the generated Go types.
  • DNSRecordType enum and DNSRecordRequest/DNSRecord shapes line up with the Go types (A/AAAA/CNAME; name, type, content, ttl, plus id on the read model).

No inconsistencies spotted between these schemas and the generated api package.

Comment thread management/server/store/sql_store.go Outdated
# Conflicts:
#	management/internals/controllers/network_map/controller/controller.go
#	management/internals/controllers/network_map/controller/repository.go
#	management/internals/server/modules.go
#	management/server/activity/codes.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
management/server/account_test.go (1)

399-401: Avoid hard‑coded DNS domain in test; use account.Domain

The new GetPeerNetworkMap signature usage looks correct, but you can tighten the test by deriving the domain from the account instead of repeating the literal:

-        customZone := account.GetPeersCustomZone(context.Background(), "netbird.io")
-        networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, "netbird.io", []nbdns.CustomZone{customZone}, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
+        customZone := account.GetPeersCustomZone(context.Background(), account.Domain)
+        networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, account.Domain, []nbdns.CustomZone{customZone}, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
shared/management/http/api/openapi.yml (1)

4604-4787: Add 400 responses to DNS Zones list endpoints for consistency

Most list endpoints in this spec expose a 400 response (e.g. /api/dns/nameservers, /api/peers, etc.), but:

  • GET /api/dns/zones
  • GET /api/dns/zones/{zoneId}/records

currently only define 200, auth, and 500 (plus 404 on the records list).

For consistency with the rest of the API surface and to accommodate potential client or path validation errors, consider adding 400 to both:

   /api/dns/zones:
     get:
@@
       responses:
         '200':
           description: A JSON Array of DNS Zones
           content:
             application/json:
               schema:
                 type: array
                 items:
                   $ref: '#/components/schemas/Zone'
+        '400':
+          "$ref": "#/components/responses/bad_request"
         '401':
           "$ref": "#/components/responses/requires_authentication"
@@
   /api/dns/zones/{zoneId}/records:
     get:
@@
       responses:
         '200':
           description: A JSON Array of DNS Records
           content:
             application/json:
               schema:
                 type: array
                 items:
                   $ref: '#/components/schemas/DNSRecord'
+        '400':
+          "$ref": "#/components/responses/bad_request"
         '401':
           "$ref": "#/components/responses/requires_authentication"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 604085d and 783a120.

📒 Files selected for processing (11)
  • management/internals/controllers/network_map/controller/controller.go (14 hunks)
  • management/internals/controllers/network_map/controller/repository.go (3 hunks)
  • management/internals/server/boot.go (1 hunks)
  • management/internals/server/modules.go (2 hunks)
  • management/server/account_test.go (1 hunks)
  • management/server/activity/codes.go (2 hunks)
  • management/server/http/handler.go (3 hunks)
  • management/server/http/handlers/peers/peers_handler.go (2 hunks)
  • management/server/http/testing/testing_tools/channel/channel.go (2 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
  • shared/management/http/api/types.gen.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • management/server/http/handlers/peers/peers_handler.go
  • management/server/http/testing/testing_tools/channel/channel.go
  • management/server/http/handler.go
  • management/internals/server/boot.go
  • management/internals/controllers/network_map/controller/repository.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/internals/server/modules.go
  • shared/management/http/api/openapi.yml
  • management/internals/controllers/network_map/controller/controller.go
  • shared/management/http/api/types.gen.go
  • management/server/account_test.go
🧬 Code graph analysis (2)
management/internals/server/modules.go (5)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/server/container.go (1)
  • Create (6-10)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (23-29)
management/internals/modules/zones/records/manager/manager.go (1)
  • NewManager (25-31)
management/server/account_test.go (2)
dns/dns.go (1)
  • CustomZone (43-52)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Signal / Unit (amd64)
  • GitHub Check: Signal / Unit (386)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Android / Build
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: iOS / Build
  • GitHub Check: Darwin
  • GitHub Check: release_ui_darwin
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: release
🔇 Additional comments (14)
management/internals/server/modules.go (1)

10-13: Zones/records manager wiring is consistent and correct

The new ZonesManager and RecordsManager accessors follow the existing lazy Create pattern, inject the expected dependencies (store, account, permissions), and mirror NetworksManager/ResourcesManager style. No changes needed.

Also applies to: 126-136

shared/management/http/api/openapi.yml (1)

28-29: DNS Zones tagging and schemas look coherent

The new DNS Zones tag plus ZoneRequest/Zone and DNS record schemas are internally consistent and match existing patterns:

  • Zone correctly composes ZoneRequest via allOf, so enabled is defined where it’s required.
  • ZoneRequest vs Zone separation (optional enabled on request, required on response) is a nice touch.
  • DNSRecordType/DNSRecordRequest/DNSRecord cleanly model the limited A/AAAA/CNAME surface.

No changes needed here.

Also applies to: 1710-1803

shared/management/http/api/types.gen.go (4)

15-20: LGTM: DNS record type constants and enum definition.

The DNS record type constants cover the essential record types (A, AAAA, CNAME) for custom DNS zones. The string-based enum follows the existing pattern in this generated file.

Also applies to: 450-451


417-448: LGTM: DNS record models follow REST conventions.

The separation between DNSRecord (with ID) and DNSRecordRequest (without ID) follows standard RESTful API patterns. The field types are appropriate for DNS record data.


1909-1949: LGTM: Zone models are well-structured.

The Zone and ZoneRequest structures provide comprehensive fields for DNS zone management. The use of DistributionGroups for access control aligns with NetBird's group-based architecture, and embedding Records in the zone response is good for API ergonomics.


2035-2045: LGTM: Request body type aliases are consistent.

The request body type aliases follow the established oapi-codegen pattern and cover all necessary CRUD operations for DNS zones and records.

management/internals/controllers/network_map/controller/controller.go (7)

15-15: LGTM: New imports support DNS zones feature.

The addition of miekg/dns (standard Go DNS library) and the internal zones/records modules are necessary for the multi-zone DNS support being added.

Also applies to: 24-25


202-206: LGTM: Comprehensive error handling and clean zone filtering pattern.

The account zones retrieval includes proper error logging and propagation. The pattern of filtering zones by peer groups and then appending the peers custom zone is clear and correctly uses the goroutine parameter p.

Also applies to: 231-233, 238-238, 240-240


348-356: LGTM: Consistent zone handling pattern in UpdateAccountPeer.

The zone retrieval and filtering logic matches the pattern in sendUpdateAccountPeers, maintaining consistency across the codebase.

Also applies to: 360-360, 362-362


455-465: LGTM: Zone handling consistent in GetValidatedPeerWithMap.

The zone filtering follows the established pattern with appropriate error handling.

Also applies to: 476-476, 478-478


501-501: LGTM: Function signature updated for multi-zone support.

The change from a single custom zone to a slice of custom zones enables multiple DNS zones per account. Extracting dnsDomain from account settings before passing to the network map builder is appropriate.

Also applies to: 512-513


834-843: LGTM: Zone handling in GetNetworkMap follows the pattern.

The zone retrieval and filtering logic is consistent with the other updated functions.

Also applies to: 854-854, 856-856


871-928: LGTM: Well-structured zone filtering implementation.

The filterPeerAppliedZones function correctly implements multi-zone DNS filtering:

  • Early return for peers without groups is efficient
  • Filtering logic correctly checks zone enabled status and non-empty records (consistent with the learning that zones without records don't affect network maps)
  • Distribution group access check properly evaluates peer membership
  • DNS record type mapping covers the standard types (A, AAAA, CNAME) with appropriate warning logging for unknown types
  • Use of dns.Fqdn() ensures proper DNS format for names and domains
  • The function is defensive and handles edge cases appropriately

Based on learnings, zones without records are correctly filtered out.

management/server/activity/codes.go (1)

184-190: LGTM: DNS activity codes follow established patterns.

The six new activity constants (DNS zone and record create/update/delete) use sequential values and follow the existing naming conventions. The activity map entries have clear, descriptive messages and use hierarchical code strings (dns.zone.* and dns.zone.record.*) consistent with other activity codes in the system.

Also applies to: 302-308

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
management/server/store/sql_store.go (1)

4141-4149: Consider omitting associations in UpdateZone.

UpdateZone uses Select("*").Save(zone) without explicitly omitting associations. If a zone is retrieved via GetZoneByID (which preloads Records), then modified and saved, GORM might attempt to update the nested records as well. For consistency with similar operations (e.g., UpdateGroup at line 3163) and to prevent unintended association updates, consider using Omit(clause.Associations):

 func (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {
-	result := s.db.Select("*").Save(zone)
+	result := s.db.Omit(clause.Associations).Save(zone)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to update zone to store: %v", result.Error)
 		return status.Errorf(status.Internal, "failed to update zone to store")

This ensures that records are only modified through their dedicated CRUD operations (CreateDNSRecord, UpdateDNSRecord, etc.).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 783a120 and d4416ed.

📒 Files selected for processing (4)
  • management/internals/modules/zones/records/manager/manager.go (1 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/store/sql_store_test.go (2 hunks)
  • management/server/store/store.go (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/store/sql_store_test.go
  • management/server/store/store.go
  • management/server/store/sql_store.go
  • management/internals/modules/zones/records/manager/manager.go
🧬 Code graph analysis (2)
management/server/store/store.go (3)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1931)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/server/store/sql_store.go (4)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (4)
  • Error (54-57)
  • Errorf (70-75)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
management/server/store/store.go (2)
  • LockingStrength (42-42)
  • LockingStrengthNone (49-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Management / Integration (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Signal / Unit (amd64)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: Linux
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
🔇 Additional comments (16)
management/internals/modules/zones/records/manager/manager.go (6)

19-31: LGTM!

Clean constructor and dependency injection pattern for the records manager.


33-55: LGTM!

Read operations correctly validate permissions and use appropriate locking strength for queries.


57-101: LGTM!

The CreateRecord implementation correctly:

  • Validates permissions before mutation
  • Uses transactional boundaries with appropriate locking
  • Validates record conflicts within the transaction
  • Increments network serial to propagate changes
  • Triggers peer updates asynchronously (as addressed from previous review)

103-160: LGTM!

UpdateRecord correctly:

  • Fetches existing record with appropriate locking
  • Only re-validates conflicts when name/type/content changes (TTL changes correctly bypass conflict validation)
  • Increments serial and triggers peer updates for all changes including TTL-only

162-207: LGTM!

DeleteRecord correctly handles transactional deletion with proper locking, event logging, and peer notification.


209-236: LGTM!

The zone membership check correctly enforces label boundary (exact match or proper subdomain with dot prefix), and the CNAME/duplicate conflict detection logic is sound.

management/server/store/sql_store_test.go (3)

25-26: LGTM!

Imports correctly added for the new zones and records test coverage.


3723-3879: LGTM!

Comprehensive test coverage for zone CRUD operations with proper table-driven tests for error cases and field validation.


3881-4126: LGTM!

Thorough test coverage for DNS record operations including:

  • Multiple record types (A, AAAA, CNAME)
  • Name-based filtering
  • Bulk deletion
  • Proper error handling verification
management/server/store/store.go (3)

26-27: LGTM!

Imports correctly added for the new zone and record types used in the interface.


208-213: LGTM!

Zone interface methods follow the established patterns in the Store interface with consistent parameter ordering and locking strength handling for read operations.


215-222: LGTM!

DNS record interface methods are comprehensive and properly scoped to zones:

  • CRUD operations with correct parameter ordering
  • Zone-scoped bulk operations (GetZoneDNSRecords, DeleteZoneDNSRecords)
  • Name-based filtering (GetZoneDNSRecordsByName)
management/server/store/sql_store.go (4)

30-31: LGTM: Imports and migration setup.

The new zones and records imports are correctly added, and the entities are properly registered in AutoMigrate for schema management.

Also applies to: 118-118


4134-4134: Previous review issues resolved.

The typo in the error message (line 4134) has been corrected, and DeleteDNSRecord now properly scopes deletion by all three identifiers (accountID, zoneID, and recordID), fixing the data integrity concern from the earlier review.

Also applies to: 4221-4222


4131-4139: LGTM: Zone and DNS record CRUD operations.

The remaining CRUD implementations follow consistent patterns throughout the file:

  • Proper locking strength support on retrieval operations
  • Appropriate error logging and status error types
  • Correct use of NewZoneNotFoundError and NewDNSRecordNotFoundError
  • Consistent query conditions (e.g., accountAndIDQueryCondition for scoped operations)
  • DeleteZoneDNSRecords correctly omits RowsAffected check since deleting zero records for a zone is valid

The implementations integrate well with the existing store architecture.

Also applies to: 4165-4199, 4201-4219, 4235-4295


4151-4163: The zone deletion is properly handled at the manager layer. Before DeleteZone is called in the store (line 162 in manager.go), DeleteZoneDNSRecords is invoked first (line 157), ensuring all associated DNS records are removed within a transaction. This orchestration prevents both foreign key constraint errors and orphaned records. The simple delete implementation in sql_store.go is appropriate since cascade logic is correctly managed at the manager layer.

Likely an incorrect or invalid review comment.

Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
shared/management/http/api/openapi.yml (2)

4608-4658: /api/dns/zones collection endpoint is well-shaped; consider 400 for consistency

The list/create endpoints for zones mirror existing style (security, tags, schemas) and look correct. One minor consistency nit: most other list endpoints also declare a 400 response; you might want to add that here for symmetry with the rest of the API surface.


4759-4948: Record-level endpoints are consistent; small response-code/enums polish possible

The nested record endpoints under /api/dns/zones/{zoneId} are modeled consistently with other resources (path params, security, 404 handling, and use of DNSRecord / DNSRecordRequest). Two optional improvements:

  • Add 400 responses to the new GET endpoints (list + single record) to mirror other list/get paths.
  • Consider extending the Event.activity_code enum to include the new dns.zone.* and dns.zone.record.* codes used in activity/codes.go, so the audit/event schema fully reflects all possible values.
management/server/activity/codes.go (1)

307-313: Activity map entries for DNS zones/records align with naming conventions

Messages and string codes (dns.zone.*, dns.zone.record.*) match the rest of the activity namespace. As a follow-up, updating the OpenAPI activity_code enum to list these codes would keep the external API docs in sync with the backend.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 176fd64 and 9e13c94.

📒 Files selected for processing (6)
  • management/internals/server/boot.go
  • management/internals/shared/grpc/conversion.go
  • management/server/account.go
  • management/server/activity/codes.go
  • shared/management/http/api/openapi.yml
  • shared/management/http/api/types.gen.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/internals/shared/grpc/conversion.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/account.go
  • shared/management/http/api/types.gen.go
  • shared/management/http/api/openapi.yml
🧬 Code graph analysis (2)
management/server/account.go (1)
shared/management/status/error.go (5)
  • FromError (78-87)
  • Type (46-46)
  • NotFound (21-21)
  • Errorf (70-75)
  • InvalidArgument (27-27)
management/internals/server/boot.go (1)
management/server/http/handler.go (1)
  • NewAPIHandler (57-146)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: Windows
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
  • GitHub Check: Linux
  • GitHub Check: Build Cache
  • GitHub Check: Darwin
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
🔇 Additional comments (13)
management/server/account.go (3)

298-298: LGTM: Transaction properly propagated to validation.

Passing the transaction to validateSettingsUpdate ensures that the DNS zone conflict check (added below) executes within the same transaction context, maintaining atomicity and consistency.


390-390: LGTM: Function signature correctly updated.

The signature update to accept the transaction parameter is consistent with the codebase pattern and enables transaction-aware validation logic.


404-414: DNS domain conflict check uses proper error handling.

The error handling correctly distinguishes NotFound errors from actual store failures, allowing the conflict check to proceed when no zone exists while properly returning errors for other cases. This properly addresses the previous review concern.

Consider normalizing domain case before passing to GetZoneByDomain. DNS labels are treated in a case insensitive fashion, and normalizing to a consistent case prevents false negatives where domains differing only in case (e.g., "Example.com" vs "example.com") would not be detected as conflicts.

shared/management/http/api/types.gen.go (4)

15-20: DNSRecordType constants match the OpenAPI enum

DNSRecordTypeA/AAAA/CNAME are correctly declared as DNSRecordType values and align with the DNSRecordType enum in the spec. Nothing to change here.


420-455: DNSRecord/DNSRecordRequest shape and tags look consistent with schema

Fields (name, type, content, ttl, plus id on DNSRecord) and JSON tags match the OpenAPI definitions, with DNSRecordType correctly reused. This looks good for client/server codegen.


1912-1952: Zone vs ZoneRequest: enabled semantics and JSON mapping are sound

Using Enabled *bool on ZoneRequest (optional with default in the schema) and Enabled bool on Zone (required via allOf) is a good pattern and aligns with the OpenAPI. The rest of the fields and tags are consistent.


2038-2048: Request-body aliases keep the public API surface clear

The new JSON request body aliases (PostApiDnsZonesJSONRequestBody, PutApiDnsZonesZoneIdJSONRequestBody, and the record counterparts) are correctly wired to ZoneRequest/DNSRecordRequest and follow the existing pattern.

shared/management/http/api/openapi.yml (4)

28-29: Tagging DNS Zones separately improves API discoverability

Adding the dedicated DNS Zones tag fits well with the existing tagging scheme and keeps custom zone operations grouped logically.


1714-1763: ZoneRequest/Zone schemas are coherent and match usage

  • ZoneRequest has the right required set (name, domain, enable_search_domain, distribution_groups) and an optional enabled with a sensible default.
  • Zone composes ZoneRequest via allOf and adds id and records, correctly requiring id, enabled, and records.

This structure should generate the intended Go types and client APIs.


1764-1807: DNSRecord schemas now internally consistent with no stray required fields*

DNSRecordType enum, DNSRecordRequest, and DNSRecord (id + allOf with request) line up correctly, and the earlier “required but undefined enabled” issue is gone. The validation constraints on content and ttl also look reasonable.


4660-4757: Single-zone CRUD endpoints look correct and error handling is aligned

Path params, security, and success/404 responses for /api/dns/zones/{zoneId} are consistent with existing resources. This should integrate cleanly with generated clients and server handlers.

management/server/activity/codes.go (1)

186-192: New DNS zone/record Activity IDs are appended safely

The new Activity constants for DNS zones and records use consecutive values after 92 and don’t disturb existing IDs, honoring the “must not be changed” constraint. This avoids breaking existing stored activity data.

management/internals/server/boot.go (1)

96-96: ZonesManager() and RecordsManager() are properly implemented and correctly integrated.

Both methods are defined in management/internals/server/modules.go and follow the same Create() pattern used by other managers. The parameter order in the NewAPIHandler call is correct, with ZonesManager() and RecordsManager() at positions 15 and 16 respectively, matching the function signature.

crn4
crn4 previously approved these changes Dec 22, 2025
# Conflicts:
#	management/internals/controllers/network_map/controller/controller.go
#	management/server/account_test.go
#	management/server/http/handlers/peers/peers_handler.go
#	management/server/store/sql_store.go
#	management/server/store/sql_store_test.go
#	management/server/store/store.go
#	management/server/types/account.go
#	management/server/types/networkmap_golden_test.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @shared/management/http/api/openapi.yml:
- Around line 28-29: The Zone schema uses an allOf combining an inline object
and ZoneRequest but the inline object's required list improperly includes
"enabled" which is declared only in ZoneRequest; remove "enabled" from the
inline object's required array (leave "id" and "records") or alternatively make
"enabled" required inside the ZoneRequest schema so required only references
properties declared in the same schema; update the Zone definition (the inline
object under Zone allOf and/or ZoneRequest) to ensure each required entry points
to a property defined in that same schema.
♻️ Duplicate comments (4)
management/server/store/sql_store.go (4)

4139-4147: Tighten UpdateZone to scope by (account_id, id) and avoid blanket Select("*")

Select("*").Save(zone) can unintentionally upsert or overwrite immutable fields and isn’t constrained by account_id. It would be safer to:

  • add a nil guard for zone, and
  • use a scoped update on mutable columns only, keyed by both account_id and id (similar to other update helpers in this file).
Example refactor
 func (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {
-	result := s.db.Select("*").Save(zone)
+	if zone == nil {
+		return status.Errorf(status.InvalidArgument, "zone is nil")
+	}
+
+	result := s.db.
+		Model(&zones.Zone{}).
+		Where("account_id = ? AND id = ?", zone.AccountID, zone.ID).
+		Omit("id", "account_id").
+		Updates(zone)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to update zone to store: %v", result.Error)
 		return status.Errorf(status.Internal, "failed to update zone to store")
 	}

4183-4196: Normalize domain in GetZoneByDomain for case-insensitive lookups

Account-domain queries already normalize with strings.ToLower(domain); GetZoneByDomain currently compares domain as-is, which can introduce subtle case-sensitivity bugs if callers pass mixed-case domains.

Example adjustment
 func (s *SqlStore) GetZoneByDomain(ctx context.Context, accountID, domain string) (*zones.Zone, error) {
-	var zone *zones.Zone
-	result := s.db.Where("account_id = ? AND domain = ?", accountID, domain).First(&zone)
+	var zone *zones.Zone
+	result := s.db.
+		Where("account_id = ? AND domain = ?", accountID, strings.ToLower(domain)).
+		First(&zone)

4214-4222: Add basic validation in CreateDNSRecord (nil/required fields)

CreateDNSRecord currently forwards any value to GORM; a nil record or missing AccountID/ZoneID would surface as an internal DB error rather than a clear invalid-argument failure.

Example guardrails
 func (s *SqlStore) CreateDNSRecord(ctx context.Context, record *records.Record) error {
-	result := s.db.Create(record)
+	if record == nil {
+		return status.Errorf(status.InvalidArgument, "dns record is nil")
+	}
+	if record.AccountID == "" || record.ZoneID == "" {
+		return status.Errorf(status.InvalidArgument, "dns record must have account_id and zone_id")
+	}
+
+	result := s.db.Create(record)

4224-4232: Scope UpdateDNSRecord like deletes and add nil guard; other DNS record methods look solid

UpdateDNSRecord still uses Select("*").Save(record) without scoping by account_id/zone_id or guarding record == nil, while the surrounding methods already do the right thing:

  • DeleteDNSRecord now scopes by (account_id, zone_id, id) and returns a typed not-found error.
  • GetDNSRecordByID and the GetZoneDNSRecords* helpers use consistent (account_id, zone_id, …) predicates.

Bringing UpdateDNSRecord in line with that pattern would avoid unintended upserts or cross-tenant updates.

Example refactor
 func (s *SqlStore) UpdateDNSRecord(ctx context.Context, record *records.Record) error {
-	result := s.db.Select("*").Save(record)
+	if record == nil {
+		return status.Errorf(status.InvalidArgument, "dns record is nil")
+	}
+
+	result := s.db.
+		Model(&records.Record{}).
+		Where("account_id = ? AND zone_id = ? AND id = ?", record.AccountID, record.ZoneID, record.ID).
+		Omit("id", "account_id", "zone_id").
+		Updates(record)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to update dns record to store: %v", result.Error)
 		return status.Errorf(status.Internal, "failed to update dns record to store")
 	}

Also applies to: 4234-4246, 4268-4282, 4284-4298, 4300-4308

🧹 Nitpick comments (6)
management/internals/controllers/network_map/controller/controller.go (1)

345-349: Code duplication - consider helper method.

The account zones fetching pattern is repeated in four different functions. While the error handling is consistent and correct, consider extracting this into a helper method for better maintainability:

func (c *Controller) getAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) {
    accountZones, err := c.repo.GetAccountZones(ctx, accountID)
    if err != nil {
        log.WithContext(ctx).Errorf("failed to get account zones: %v", err)
        return nil, fmt.Errorf("failed to get account zones: %v", err)
    }
    return accountZones, nil
}

This is an optional refactoring for future consideration.

Also applies to: 450-454, 826-830

management/server/store/sql_store_test.go (2)

3890-4114: Zone store tests provide solid CRUD and lookup coverage

The new TestSqlStore_*Zone tests cover creation, ID-based lookup (including empty/non-existing IDs), per-account listing, domain-based lookup (including cross-account and empty-domain NotFound cases), update, and deletion with proper status.NotFound assertions. This gives good confidence in the new zone persistence API.

As a follow‑up, you might later add a small test for GetAccountZones against a non‑existent account ID to lock in the “no zones, no error” behavior, if that’s part of the contract.


4116-4361: DNS record store tests thoroughly exercise record lifecycle

The DNS record tests comprehensively cover record creation, ID lookup (including missing/empty IDs), listing all records for a zone, name-based filtering, update, single delete, and bulk DeleteZoneDNSRecords, all scoped by accountID and zone.ID with correct NotFound expectations. The use of multiple record types (A, AAAA, CNAME) is a nice touch.

If DeleteZone is expected to cascade-record deletion automatically (via FKs or logic), consider an additional test that creates records, deletes the zone, and asserts records are gone; otherwise the current explicit DeleteZoneDNSRecords coverage is sufficient.

management/server/account_test.go (1)

2099-2126: DNS domain vs. custom zone conflict behavior is well covered

TestDefaultAccountManager_UpdateAccountSettings_DNSDomainConflict nicely pins down the new validation: creating a custom zone for custom.example.com and then attempting to set Settings.DNSDomain to the same value correctly results in an error, and the assertion on the conflict message makes the intent explicit. Using Store.CreateZone with an empty records list ensures the test doesn’t accidentally impact network maps, which is consistent with the behavior that zones without records are filtered out. Based on learnings, this is a safe way to exercise the conflict check without side effects.

You might consider also asserting on the status error type (via status.FromError) to lock in the semantic error class in addition to the message substring, but that’s optional.

management/server/types/networkmap_golden_test.go (1)

72-72: GetPeerNetworkMap signature updates in tests look consistent

All updated call sites correctly pass the new peersCustomZone and accountZones parameters (dns.CustomZone{} and nil respectively), keeping the golden/benchmark behavior focused on existing routing logic while allowing future extension for account zones.

Also applies to: 108-109, 144-145, 204-205, 272-273, 323-324, 333-334, 398-399, 479-480, 607-608, 668-669, 733-734, 800-801, 850-851, 860-861

management/server/types/account.go (1)

273-284: Account DNS zones handling is correct; minor optional refinements

The new accountZones flow in GetPeerNetworkMap, combined with filterPeerAppliedZones, cleanly:

  • uses the requesting peer’s group set for access control,
  • skips disabled zones and zones without records, and
  • maps A/AAAA/CNAME records into nbdns.SimpleRecord with correct TTL and search-domain behavior.

This matches the design that zones without records don’t affect the network map. As optional polish, you could:

  • reuse the already computed peerGroups in getPeerNSGroups to avoid a second full group scan, and
  • (if ever needed) skip appending zones whose simpleRecords slice ends up empty after filtering unknown types.

Based on learnings, empty-account zones being ignored here ensures CreateZone doesn’t require peer-map updates.

Also applies to: 299-323, 339-344, 1870-1892, 1894-1954

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e13c94 and a3b9503.

📒 Files selected for processing (13)
  • management/internals/controllers/network_map/controller/controller.go
  • management/internals/shared/grpc/conversion.go
  • management/server/account.go
  • management/server/account_test.go
  • management/server/http/handlers/peers/peers_handler.go
  • management/server/store/sql_store.go
  • management/server/store/sql_store_test.go
  • management/server/store/store.go
  • management/server/types/account.go
  • management/server/types/account_test.go
  • management/server/types/networkmap_golden_test.go
  • shared/management/http/api/openapi.yml
  • shared/management/http/api/types.gen.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/server/http/handlers/peers/peers_handler.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/account.go
  • management/server/account_test.go
  • shared/management/http/api/types.gen.go
  • management/server/store/sql_store_test.go
  • management/internals/controllers/network_map/controller/controller.go
  • shared/management/http/api/openapi.yml
  • management/server/types/networkmap_golden_test.go
  • management/server/store/store.go
  • management/server/types/account.go
  • management/server/types/account_test.go
  • management/server/store/sql_store.go
🧬 Code graph analysis (7)
management/server/account_test.go (2)
management/server/store/store.go (1)
  • Store (52-225)
management/server/types/settings.go (1)
  • Settings (10-58)
management/internals/controllers/network_map/controller/controller.go (2)
shared/management/proto/management.pb.go (6)
  • NetworkMap (1915-1947)
  • NetworkMap (1962-1962)
  • NetworkMap (1977-1979)
  • CustomZone (2868-2877)
  • CustomZone (2892-2892)
  • CustomZone (2907-2909)
shared/management/http/api/types.gen.go (1)
  • Zone (1960-1981)
management/server/types/networkmap_golden_test.go (1)
shared/management/proto/management.pb.go (3)
  • CustomZone (2868-2877)
  • CustomZone (2892-2892)
  • CustomZone (2907-2909)
management/server/store/store.go (3)
shared/management/http/api/types.gen.go (1)
  • Zone (1960-1981)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/server/types/account.go (4)
management/server/peer/peer.go (1)
  • Peer (16-58)
management/server/networks/resources/types/resource.go (1)
  • Domain (25-25)
dns/dns.go (3)
  • CustomZone (43-52)
  • SimpleRecord (55-66)
  • DefaultClass (21-21)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
management/server/types/account_test.go (6)
shared/management/http/api/types.gen.go (1)
  • Zone (1960-1981)
management/server/types/account.go (1)
  • LookupMap (62-62)
shared/management/proto/management.pb.go (6)
  • CustomZone (2868-2877)
  • CustomZone (2892-2892)
  • CustomZone (2907-2909)
  • SimpleRecord (2940-2950)
  • SimpleRecord (2965-2965)
  • SimpleRecord (2980-2982)
management/server/networks/resources/types/resource.go (1)
  • Domain (25-25)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
management/server/store/sql_store.go (4)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (5)
  • Error (54-57)
  • Errorf (70-75)
  • Internal (24-24)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
management/server/store/store.go (2)
  • LockingStrength (42-42)
  • LockingStrengthNone (49-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: release_ui
  • GitHub Check: release_ui_darwin
  • GitHub Check: Build Cache
  • GitHub Check: release
  • GitHub Check: FreeBSD Port / Build & Test
  • GitHub Check: Android / Build
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: Linux
🔇 Additional comments (15)
management/server/account.go (2)

298-298: LGTM!

The addition of the transaction parameter to validateSettingsUpdate enables database queries within the transaction context, ensuring proper isolation for the DNS domain conflict check.

Also applies to: 390-390


404-414: LGTM! Proper error handling for DNS domain conflict check.

The implementation correctly:

  • Validates domain conflicts by querying existing zones within the transaction
  • Distinguishes NotFound errors (no conflict) from genuine database errors
  • Propagates unexpected errors for proper handling
  • Only returns a conflict error when a zone actually exists

This addresses the concern raised in the previous review about error handling.

management/internals/controllers/network_map/controller/controller.go (3)

23-23: LGTM! Improved naming clarity.

The addition of the zones import and the variable rename from customZone to peersCustomZone improves code clarity by explicitly distinguishing peer-specific DNS zones from account-wide zones.

Also applies to: 179-179


201-205: LGTM! Proper account zones retrieval.

The account zones are fetched once before iterating over peers, which is efficient. Error handling is appropriate with both logging and error propagation.


233-235: LGTM! Consistent integration of account zones.

The accountZones parameter is consistently added across all network map generation paths (both experimental and non-experimental). The signature updates maintain consistency throughout the call chain.

Also applies to: 354-356, 468-470, 488-506, 844-846

shared/management/http/api/types.gen.go (1)

15-20: Generated file - no review needed.

This file is code-generated by oapi-codegen from the OpenAPI specification. The new DNS record types, Zone types, and request body aliases correctly reflect the API surface expansion for DNS zones and records management.

Also applies to: 423-457, 1959-1999, 2085-2095

management/internals/shared/grpc/conversion.go (1)

375-380: LGTM! Straightforward field addition.

The SearchDomainDisabled field is correctly added to the proto conversion, following the same pattern as other fields in the convertToProtoCustomZone function.

management/server/types/account_test.go (1)

16-17: Comprehensive coverage for filterPeerAppliedZones and DNS zones behavior

The new imports and Test_filterPeerAppliedZones look solid and give very good coverage:

  • Validate group-based access to zones (including multi-group and mixed-access scenarios).
  • Ensure disabled zones and zones with no records are filtered out, matching the intended “empty zones don’t affect network maps” invariant. Based on learnings, this aligns with the design that CreateZone doesn’t need to trigger peer updates for empty zones.
  • Check correct transformation into nbdns.CustomZone/SimpleRecord: trailing dots on domains and names, correct dns.Type* mapping for A/AAAA/CNAME, DefaultClass, TTL/RData propagation, and SearchDomainDisabled reflecting EnableSearchDomain.

No changes needed from my side here.

Also applies to: 1431-1941

management/server/store/sql_store_test.go (1)

24-27: Zones/records imports correctly scoped to new store tests

The added imports for zones and zones/records are only used in the new zone/DNS record store tests and fit the existing package layering; no issues here.

management/server/account_test.go (3)

24-31: Zones import is appropriate for account-level DNS conflict testing

Importing management/internals/modules/zones here is justified by the DNS domain conflict test below and keeps the dependency at the server layer where account settings validation lives.


400-402: GetPeerNetworkMap call updated correctly for new accountZones parameter

Passing nil for the new accountZones argument keeps existing behavior for these login‑expiration scenarios while aligning with the expanded GetPeerNetworkMap signature. The rest of the arguments (validated peers, policies, routers, active group users) remain unchanged, so the test’s expectations around online/offline peers stay valid.


1680-1691: GetRoutesToSync tests now exercise group-aware routing

Updating GetRoutesToSync to pass account.GetPeerGroups("<peer>") matches the new API and keeps the test semantics clear:

  • For peer-2, GetPeerGroups returns group1, so routes 2 and 3 (both bound to group1) are expected.
  • For peer-3, which is not in group1, passing account.GetPeerGroups("peer-3") (empty) correctly yields no routes.

This is a good, focused verification of the new group-aware filtering.

management/server/types/account.go (1)

155-170: Route selection with peerGroups aligns with intended semantics

Passing peerGroups into GetRoutesToSync and using filterRoutesByGroups to keep only ACL routes whose distribution groups intersect the requesting peer’s groups matches the function’s contract and keeps HA de-duplication intact.

Also applies to: 185-197, 299-315

management/server/store/store.go (1)

25-28: Store interface extension for zones and DNS records is coherent

The added zone and DNS record methods (including lock-strength variants and zone-scoped deletion) are consistent with the new domain models and the SqlStore implementations, and keep the interface surface aligned with other account-scoped entities.

Also applies to: 211-224

management/server/store/sql_store.go (1)

29-32: Zones/records imports and AutoMigrate wiring look good

Including zones.Zone and records.Record in the SqlStore imports and AutoMigrate set keeps schema management aligned with the rest of the store and ensures the new tables are created alongside existing account entities.

Also applies to: 119-128

Comment thread shared/management/http/api/openapi.yml
# Conflicts:
#	management/internals/controllers/network_map/controller/controller.go
#	management/internals/server/boot.go
#	management/server/activity/codes.go
#	management/server/http/handler.go
#	management/server/http/testing/testing_tools/channel/channel.go
# Conflicts:
#	management/server/types/networkmap_golden_test.go
#	management/server/types/networkmapbuilder.go
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @management/server/store/sql_store_test.go:
- Around line 4022-4493: The SQL fixture extended-store.sql used by tests like
TestSqlStore_CreateZone and TestSqlStore_CreateDNSRecord is missing the zones
and dns_records table schemas; add CREATE TABLE definitions for zones (columns:
id, account_id, name, domain, enabled, enable_search_domain,
distribution_groups, created_at/updated_at as needed) and for dns_records
(columns: id, account_id, zone_id, name, type, content, ttl,
created_at/updated_at) to the extended-store.sql fixture so the store.CreateZone
/ store.CreateDNSRecord calls in the tests can succeed; ensure column types and
nullability match the zone and record structs used by functions such as
CreateZone, GetZoneByID, CreateDNSRecord and GetDNSRecordByID.

In @management/server/store/sql_store.go:
- Around line 4270-4278: Add input validation to CreateDNSRecord: check if the
record parameter is nil and return a status.InvalidArgument error (and log) if
so; validate that required fields record.AccountID and record.ZoneID are
non-empty strings and return status.InvalidArgument with a clear message if they
are missing; only call s.db.Create(record) after these guards; keep the existing
database error handling but consider including result.Error details in the log
message.
- Around line 4185-4193: Add input validation to SqlStore.CreateZone: first
guard that the zone parameter is not nil and return a status.InvalidArgument
(logging via log.WithContext(ctx).Errorf) if it is; then validate required
fields such as zone.AccountID and zone.ID are non-empty and return
status.InvalidArgument with clear messages (and log the specific missing field)
instead of proceeding; only call s.db.Create(zone) after these checks and keep
the existing database error logging/return behavior for result.Error.
- Around line 4280-4288: The UpdateDNSRecord method currently uses Save(record)
which only checks the primary key; change it to perform a scoped update with a
WHERE on account_id, zone_id and id like other update methods: use
s.db.Model(&records.Record{}) with Where("account_id = ? AND zone_id = ? AND id
= ?", record.AccountID, record.ZoneID, record.ID) and call Updates(record) (or
Select(...) then Updates) and handle result.Error and rows affected the same way
as the other update handlers to ensure the update is constrained to the given
account/zone/id.

In @shared/management/http/api/openapi.yml:
- Around line 1784-1833: The Zone schema's inline object under allOf incorrectly
lists "enabled" in its required array even though "enabled" is declared in
ZoneRequest; remove "enabled" from the inline object's required list (leaving
only "id" and "records") so ZoneRequest owns its own required flags, update the
OpenAPI YAML accordingly for the Zone and ZoneRequest schemas, and then
regenerate the generated types (e.g., types.gen.go) to keep the code in sync.
🧹 Nitpick comments (3)
shared/management/http/api/types.gen.go (1)

2045-2085: Zone / ZoneRequest models are coherent; keep them driven by the OpenAPI schema

Zone vs ZoneRequest correctly distinguish persisted state (Enabled bool, Records []DNSRecord) from create/update payload (Enabled *bool optional, no Records). Once you adjust the Zone schema’s required list in openapi.yml as suggested, regenerate this file rather than editing it directly so these structs stay in sync.

management/server/account.go (1)

298-301: DNS domain validation and zone conflict handling are correct; consider case canonicalization

The new validateSettingsUpdate wiring from UpdateAccountSettings plus the GetZoneByDomain check correctly enforces:

  • peer login expiration bounds,
  • DNS domain syntax via isDomainValid, and
  • conflicts with existing custom DNS zones while propagating real store errors instead of swallowing them.

To avoid subtle mismatches between DNSDomain and zones.Zone.Domain (e.g. Example.com vs example.com), you may want to canonicalize newSettings.DNSDomain to lower-case before validation and lookup.

Optional: normalize DNSDomain to lower‑case before validation
 func (am *DefaultAccountManager) validateSettingsUpdate(ctx context.Context, transaction store.Store, newSettings, oldSettings *types.Settings, userID, accountID string) error {
@@
-	if newSettings.DNSDomain != "" && !isDomainValid(newSettings.DNSDomain) {
-		return status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain)
-	}
-
-	if newSettings.DNSDomain != oldSettings.DNSDomain && newSettings.DNSDomain != "" {
+	if newSettings.DNSDomain != "" {
+		newSettings.DNSDomain = strings.ToLower(newSettings.DNSDomain)
+		if !isDomainValid(newSettings.DNSDomain) {
+			return status.Errorf(status.InvalidArgument, "invalid domain \"%s\" provided for DNS domain", newSettings.DNSDomain)
+		}
+	}
+
+	if newSettings.DNSDomain != oldSettings.DNSDomain && newSettings.DNSDomain != "" {
 		existingZone, err := transaction.GetZoneByDomain(ctx, accountID, newSettings.DNSDomain)

Also applies to: 391-417

management/server/types/networkmapbuilder.go (1)

1036-1075: Account DNS zones are correctly plumbed into the peer network map

Passing peersCustomZone and accountZones through GetPeerNetworkMap into assembleNetworkMap, then combining:

  • peer-specific records via filterZoneRecordsForPeers, and
  • account-wide zones filtered by the peer’s groups via filterPeerAppliedZones(ctx, accountZones, peerGroups)

gives the expected DNS view per peer and aligns with the existing group‑scoped zone semantics (and filtering of zones without records). Based on learnings, this keeps empty zones from impacting the network map while still exposing relevant custom zones.

As a minor readability tweak, you might rename the local zones slice (e.g. to customZones) to avoid shadowing the imported zones package name in this function.

Also applies to: 1077-1168

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3b9503 and 80f5b9f.

📒 Files selected for processing (17)
  • management/internals/controllers/network_map/controller/controller.go
  • management/internals/server/boot.go
  • management/internals/server/modules.go
  • management/server/account.go
  • management/server/account_test.go
  • management/server/activity/codes.go
  • management/server/http/handler.go
  • management/server/http/testing/testing_tools/channel/channel.go
  • management/server/store/sql_store.go
  • management/server/store/sql_store_test.go
  • management/server/store/store.go
  • management/server/types/networkmap.go
  • management/server/types/networkmap_golden_test.go
  • management/server/types/networkmapbuilder.go
  • shared/management/client/rest/client.go
  • shared/management/http/api/openapi.yml
  • shared/management/http/api/types.gen.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • management/server/types/networkmap.go
  • shared/management/client/rest/client.go
  • management/server/types/networkmap_golden_test.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/internals/server/modules.go
  • management/server/http/testing/testing_tools/channel/channel.go
  • management/server/http/handler.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/server/account.go
  • management/server/types/networkmapbuilder.go
  • management/server/store/sql_store_test.go
  • management/server/account_test.go
  • management/internals/server/boot.go
  • management/server/store/store.go
  • shared/management/http/api/types.gen.go
  • management/server/store/sql_store.go
  • shared/management/http/api/openapi.yml
🧬 Code graph analysis (7)
management/internals/server/modules.go (5)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/server/container.go (1)
  • Create (6-10)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (24-31)
management/internals/modules/zones/records/manager/manager.go (1)
  • NewManager (25-31)
management/server/http/testing/testing_tools/channel/channel.go (3)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (24-31)
management/internals/modules/zones/records/manager/manager.go (1)
  • NewManager (25-31)
management/server/http/handler.go (1)
  • NewAPIHandler (62-155)
management/server/http/handler.go (4)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/modules/zones/records/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/controllers/network_map/controller/controller.go (3)
management/server/types/network.go (1)
  • NetworkMap (32-43)
shared/management/proto/management.pb.go (6)
  • NetworkMap (1915-1947)
  • NetworkMap (1962-1962)
  • NetworkMap (1977-1979)
  • CustomZone (2868-2877)
  • CustomZone (2892-2892)
  • CustomZone (2907-2909)
shared/management/http/api/types.gen.go (1)
  • Zone (2046-2067)
management/server/store/sql_store_test.go (3)
management/internals/modules/zones/zone.go (1)
  • NewZone (24-34)
shared/management/status/error.go (3)
  • Error (54-57)
  • FromError (78-87)
  • NotFound (21-21)
management/internals/modules/zones/records/record.go (1)
  • NewRecord (31-41)
management/internals/server/boot.go (2)
management/server/http/handler.go (1)
  • NewAPIHandler (62-155)
management/server/integrations/integrated_validator/interface.go (1)
  • IntegratedValidator (12-23)
management/server/store/store.go (3)
shared/management/http/api/types.gen.go (1)
  • Zone (2046-2067)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Build Cache
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: FreeBSD Port / Build & Test
  • GitHub Check: release_ui
🔇 Additional comments (29)
management/server/activity/codes.go (2)

190-196: LGTM! Clean addition of DNS zone and record activity constants.

The new constants follow existing naming conventions, use sequential values without conflicts, and are logically grouped (zone operations followed by record operations).


315-321: LGTM! Activity map entries are consistent and well-structured.

The map entries properly correspond to all six new activity constants with clear messages and well-organized code strings that follow the hierarchical pattern (dns.zone.* for zones, dns.zone.record.* for records).

management/internals/controllers/network_map/controller/controller.go (1)

177-205: DNS zones/custom zone wiring into network maps looks coherent

peersCustomZone and accountZones are computed once per account and threaded consistently through both experimental (getPeerNetworkMapExp) and legacy (account.GetPeerNetworkMap) paths, including single‑peer updates and GetNetworkMap. Error handling on GetAccountZones is uniform and early‑returns, which is appropriate for these flows.

Also applies to: 328-357, 450-473, 490-507, 823-846

shared/management/http/api/openapi.yml (2)

28-29: DNS Zones tag and endpoints align with existing API surface

The new DNS Zones tag plus /api/dns/zones and nested record routes are consistent with existing DNS/Nameserver patterns (verbs, status codes, and security), and reuse the shared Zone/DNSRecord schemas appropriately.

Also applies to: 4832-5172


1835-1877: DNSRecord schemas look consistent with generated types

DNSRecordType, DNSRecordRequest, and DNSRecord cleanly model A/AAAA/CNAME records and match the generated Go types (DNSRecord, DNSRecordRequest, DNSRecordType). No issues spotted here.

shared/management/http/api/types.gen.go (2)

15-20: DNSRecord type and constants correctly mirror the OpenAPI enum

DNSRecordType and its A/AAAA/CNAME constants, plus DNSRecord/DNSRecordRequest structs, line up with the OpenAPI schema and use appropriate JSON tags and field types (Ttl as int, Content as string).

Also applies to: 437-472


2171-2181: Request body aliases for DNS zone/record endpoints are correct

The new PostApiDnsZonesJSONRequestBody, PutApiDnsZonesZoneIdJSONRequestBody, and corresponding record aliases correctly point to ZoneRequest/DNSRecordRequest, matching the new paths.

management/server/http/testing/testing_tools/channel/channel.go (2)

98-98: Verify empty dnsDomain parameter for test scenarios.

The dnsDomain parameter is passed as an empty string to zonesManager.NewManager. Ensure that tests correctly handle scenarios where DNS domain validation or usage is required, or confirm that an empty domain is acceptable for test cases.


13-14: LGTM!

The test setup correctly instantiates the new zones and records managers and passes them to NewAPIHandler in the correct parameter positions, aligning with the updated API handler signature.

Also applies to: 98-101

management/server/http/handler.go (3)

18-21: LGTM!

The new imports for zones and records modules follow the existing import patterns and use appropriate aliases to avoid naming conflicts with parameters.


62-62: LGTM!

The function signature correctly extends NewAPIHandler to accept the new zones and records managers. The parameter positioning between settingsManager and networkMapController is logical and consistent with the existing parameter organization.


144-145: LGTM!

The endpoint registration for zones and records is correctly implemented, following the established pattern of other endpoint registrations in this function.

management/internals/server/modules.go (2)

11-14: LGTM!

The RecordsManager method correctly follows the established Create pattern used throughout this file and properly initializes the records manager with the required dependencies.

Also applies to: 172-176


166-170: The DNSDomain() method is properly defined on BaseServer (returns string from the dnsDomain field). The method call in ZonesManager() is correct.

management/internals/server/boot.go (1)

95-95: LGTM!

The API handler construction correctly includes the new zones and records managers in the proper parameter positions, aligning with the updated NewAPIHandler signature.

management/server/store/sql_store_test.go (3)

25-26: LGTM!

The imports for zones and records modules are correctly added to support the new test cases.


4022-4246: LGTM!

The zone management tests are comprehensive, covering CRUD operations, error cases, and account isolation. The test structure and assertions follow the established patterns in this file.


4248-4493: LGTM!

The DNS record tests comprehensively cover CRUD operations, filtering by name, bulk deletion, and various record types. The tests properly validate the zone-record relationship and error handling.

management/server/account_test.go (3)

400-402: Updated GetPeerNetworkMap test call matches new signature

The test now passes customZone and nil accountZones into account.GetPeerNetworkMap, which aligns with the extended GetPeerNetworkMap/builder signature while preserving previous behavior for this scenario (no account‑level zones).


1680-1693: GetRoutesToSync tests correctly supply peer groups for the requesting peer

Updating GetRoutesToSync calls to pass account.GetPeerGroups("peer-2") / account.GetPeerGroups("peer-3") matches the new API and ensures routing decisions in the test are driven by the peer’s actual group memberships instead of implicit globals.


2099-2126: DNS domain vs custom zone conflict is well covered by this test

TestDefaultAccountManager_UpdateAccountSettings_DNSDomainConflict sets up a stored custom DNS zone and then verifies that updating DNSDomain to the same value returns an error containing “conflicts with existing custom DNS zone”. This directly exercises the new GetZoneByDomain conflict check in validateSettingsUpdate and guards against regressions in the zone‑domain validation logic.

management/server/store/store.go (2)

26-27: LGTM: Import additions support new zone and record functionality.

The imports correctly reference the internal modules for zones and DNS records, enabling the store interface to expose methods for these entities.


215-228: LGTM: Store interface methods follow established patterns.

The zone and DNS record method signatures are well-designed:

  • Consistent with existing Store interface patterns
  • Proper multi-tenancy scoping via accountID
  • Flexible locking support for read operations
  • Appropriate return types for CRUD operations
management/server/store/sql_store.go (6)

30-31: LGTM: Imports and schema migration setup.

The imports are correctly added and the AutoMigrate call includes the new Zone and Record types, ensuring database schema creation.

Also applies to: 128-128


4205-4217: LGTM: DeleteZone properly scoped and handles errors.

The deletion is correctly scoped by both accountID and zoneID, with appropriate error handling and not-found detection via RowsAffected.


4219-4237: LGTM: GetZoneByID correctly retrieves zone with records.

Proper use of locking, scoping by account and zone ID, and preloading associated records. Error handling is appropriate.


4254-4268: LGTM: GetAccountZones properly retrieves all zones with records.

Correct use of locking, scoping, and preloading. Error handling is appropriate.


4290-4302: LGTM: DeleteDNSRecord now properly scoped by account, zone, and record ID.

This addresses the previous review concern about inconsistent scoping. The deletion now correctly requires all three identifiers (accountID, zoneID, recordID), ensuring data integrity and preventing accidental deletions.


4304-4364: LGTM: Remaining DNS record methods are correctly implemented.

All the remaining DNS record retrieval and deletion methods:

  • Properly scope queries by accountID and zoneID (plus recordID or name where applicable)
  • Implement consistent locking support
  • Handle errors appropriately
  • Follow established patterns in the codebase

Comment thread management/server/store/sql_store.go
Comment thread management/server/store/sql_store.go
Comment thread management/server/store/sql_store.go
Comment thread management/server/store/sql_store.go
Comment thread shared/management/http/api/openapi.yml
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @management/server/store/sql_store_test.go:
- Around line 4023-4494: The zone and DNS record tests (TestSqlStore_CreateZone,
TestSqlStore_GetZoneByID, TestSqlStore_GetAccountZones,
TestSqlStore_GetZoneByDomain, TestSqlStore_UpdateZone, TestSqlStore_DeleteZone,
TestSqlStore_CreateDNSRecord, TestSqlStore_GetDNSRecordByID,
TestSqlStore_GetZoneDNSRecords, TestSqlStore_GetZoneDNSRecordsByName,
TestSqlStore_UpdateDNSRecord, TestSqlStore_DeleteDNSRecord,
TestSqlStore_DeleteZoneDNSRecords) only run against the default engine; wrap
each test body with runTestForAllEngines so they run for SQLite/Postgres/MySQL,
i.e. replace the direct NewTestStoreFromSQL setup with runTestForAllEngines(t,
func(t *testing.T, engine string) { store, cleanup, err :=
NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql",
t.TempDir()) ... t.Cleanup(cleanup) ... existing assertions ... }) ensuring each
test uses the store created inside the wrapper and removes the original
top-level store initialization.
🧹 Nitpick comments (1)
management/server/account_test.go (1)

2099-2126: Test validates DNS domain conflict detection correctly.

The test properly exercises the conflict validation path by:

  1. Creating a custom DNS zone with a specific domain
  2. Attempting to set the account's DNSDomain to that same domain
  3. Asserting that a conflict error is returned

The zone is created without records, which correctly demonstrates that even empty zones reserve their domain names and should prevent conflicts.

Optional: Add assertion for zone creation

Consider adding a verification step after zone creation to make the test more explicit:

 	err = manager.Store.CreateZone(ctx, &zones.Zone{
 		ID:                 "test-zone-id",
 		AccountID:          accountID,
 		Name:               "Test Zone",
 		Domain:             "custom.example.com",
 		Enabled:            true,
 		EnableSearchDomain: false,
 		DistributionGroups: []string{},
 	})
 	require.NoError(t, err, "unable to create custom DNS zone")
+	
+	// Verify zone was created
+	zone, err := manager.Store.GetZoneByDomain(ctx, accountID, "custom.example.com")
+	require.NoError(t, err, "zone should exist")
+	require.NotNil(t, zone, "zone should not be nil")

This makes the test more robust by explicitly verifying the precondition before testing the conflict logic.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80f5b9f and 748903a.

📒 Files selected for processing (5)
  • management/internals/modules/zones/records/record.go
  • management/server/account.go
  • management/server/account_test.go
  • management/server/store/sql_store_test.go
  • shared/management/client/rest/client.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/internals/modules/zones/records/record.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/store/sql_store_test.go
  • management/server/account.go
  • management/server/account_test.go
🧬 Code graph analysis (3)
management/server/store/sql_store_test.go (4)
management/server/store/store.go (2)
  • NewTestStoreFromSQL (406-455)
  • LockingStrengthNone (50-50)
management/internals/modules/zones/zone.go (1)
  • NewZone (24-34)
shared/management/status/error.go (2)
  • Error (54-57)
  • FromError (78-87)
management/internals/modules/zones/records/record.go (4)
  • NewRecord (31-41)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
shared/management/client/rest/client.go (2)
shared/management/client/rest/dns.go (1)
  • DNSAPI (12-14)
shared/management/client/rest/dns_zones.go (1)
  • DNSZonesAPI (12-14)
management/server/account.go (3)
management/server/store/store.go (1)
  • Store (53-229)
management/server/permissions/modules/module.go (1)
  • Settings (9-9)
management/server/types/settings.go (1)
  • Settings (10-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: FreeBSD Port / Build & Test
  • GitHub Check: Linux
  • GitHub Check: Build Cache
  • GitHub Check: release
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: release_ui
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
🔇 Additional comments (4)
shared/management/client/rest/client.go (2)

65-68: LGTM!

The DNSZones API client field follows the established pattern used by other API clients in this struct. The documentation and naming are consistent.


120-120: LGTM!

The DNSZones initialization correctly follows the pattern used for other API clients.

management/server/account.go (1)

391-418: LGTM! DNS domain conflict validation properly implemented.

The validation correctly:

  • Passes transaction context for conflict checks within the transaction boundary
  • Distinguishes between NotFound errors (acceptable) and real store failures
  • Returns a clear conflict message when a zone with the same domain exists

The error handling matches the pattern suggested in the previous review and properly propagates store errors while allowing NotFound results to pass through.

management/server/account_test.go (1)

30-30: Function signature adjustments align with zones feature integration.

The added import and updated function calls (inserting nil for zones and adding peer groups parameters) are consistent with the broader DNS zones integration described in the PR summary. The changes maintain test validity while accommodating the new API parameters.

Also applies to: 401-401, 1680-1680, 1690-1690

Comment thread management/server/store/sql_store_test.go
Comment thread management/server/store/sql_store_test.go
Comment on lines 376 to 380
protoZone := &proto.CustomZone{
Domain: zone.Domain,
Records: make([]*proto.SimpleRecord, 0, len(zone.Records)),
Domain: zone.Domain,
Records: make([]*proto.SimpleRecord, 0, len(zone.Records)),
SearchDomainDisabled: zone.SearchDomainDisabled,
}
Copy link
Copy Markdown
Collaborator

@lixmal lixmal Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also set SkipPTRProcess, no? And the zone should have it set when this is a custom zone (= not the "netbird.cloud" one)

# Conflicts:
#	management/internals/shared/grpc/conversion.go
Signed-off-by: bcmmbaga <bethuelmbaga12@gmail.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
management/server/account.go (1)

286-350: Potential nil/behavior bug: default newSettings.Extra before calling validateSettingsUpdate.
Right now validation runs while newSettings.Extra may still be nil (the defaulting happens later).

Proposed fix
diff --git a/management/server/account.go b/management/server/account.go
@@
 		oldSettings, err = transaction.GetAccountSettings(ctx, store.LockingStrengthUpdate, accountID)
 		if err != nil {
 			return err
 		}
 
+		if newSettings.Extra == nil {
+			newSettings.Extra = oldSettings.Extra
+		}
+
 		if err = am.validateSettingsUpdate(ctx, transaction, newSettings, oldSettings, userID, accountID); err != nil {
 			return err
 		}
@@
-		if newSettings.Extra == nil {
-			newSettings.Extra = oldSettings.Extra
-		}
+		// newSettings.Extra is normalized above (before validation)
🤖 Fix all issues with AI agents
In @management/server/store/sql_store_test.go:
- Around line 4205-4233: Add an assertion in TestSqlStore_UpdateZone to verify
the zone's AccountID was not changed by the update: after retrieving updatedZone
via store.GetZoneByID, assert that updatedZone.AccountID equals the original
accountID (the local variable used to create zone) to ensure UpdateZone did not
move the zone across accounts.
- Around line 4384-4415: Add assertions to ensure each returned record belongs
to the requested zone: after calling GetZoneDNSRecordsByName and iterating
recordsByName, assert that r.ZoneID (or the struct field that holds the zone
identifier) equals zone.ID for every record; this will catch regressions where
the query returns same-name records from the wrong zone.
- Around line 4031-4052: Add an assertion to ensure the created zone was
persisted to the correct account: after fetching savedZone in
TestSqlStore_CreateZone (which calls store.CreateZone and store.GetZoneByID),
assert that savedZone.AccountID equals accountID to detect any cross-account
persistence regressions.

In @management/server/store/sql_store.go:
- Around line 4271-4303: UpdateDNSRecord currently calls
s.db.Select("*").Save(record) which allows mutated AccountID/ZoneID/ID on the
input record to move records across scopes; change UpdateDNSRecord to perform a
scoped update using the record identity fields (AccountID, ZoneID, ID) in a
Where clause and call Updates (or
Model(...).Where(...).Select(...).Updates(...)) so only the intended row is
modified and AccountID/ZoneID/ID are not used to relocate the row; reference
UpdateDNSRecord and the fields AccountID, ZoneID, ID and use the same pattern as
DeleteDNSRecord/SaveAccountSettings to ensure correct scoping.
🧹 Nitpick comments (6)
management/server/account.go (1)

391-418: DNSDomain conflict check against existing zones is a good guardrail; consider whether disabled zones should block.
If disabled zones (or zones with no records) are meant to be “reserved” anyway, this is fine; otherwise you may want existingZone.Enabled (and possibly len(existingZone.Records)) to influence the conflict decision.

management/server/account_test.go (1)

2099-2126: Prefer asserting status type (InvalidArgument) instead of only matching error text.
This will keep the test stable if the wording changes while semantics stay the same.

Proposed tweak
diff --git a/management/server/account_test.go b/management/server/account_test.go
@@
 	_, err = manager.UpdateAccountSettings(ctx, accountID, userID, &types.Settings{
 		DNSDomain:                  "custom.example.com",
 		PeerLoginExpiration:        time.Hour,
 		PeerLoginExpirationEnabled: false,
 		Extra:                      &types.ExtraSettings{},
 	})
 	require.Error(t, err, "expecting to fail when DNS domain conflicts with custom zone")
-	assert.Contains(t, err.Error(), "conflicts with existing custom DNS zone")
+	if s, ok := status.FromError(err); ok {
+		assert.Equal(t, status.InvalidArgument, s.Type())
+	}
+	assert.Contains(t, err.Error(), "conflicts with existing custom DNS zone")
management/server/store/store.go (1)

215-228: Consider splitting zone/record methods into smaller embedded interfaces (to reduce churn).
Optional, but it can make mocking and alternative store implementations less painful as the API grows. Also, consider aligning GetZoneByDomain with other getters by accepting lockStrength for consistency (it’s now used in a txn validation path).

management/server/store/sql_store_test.go (1)

4109-4135: Fixture-coupled count assertion may be brittle.
assert.GreaterOrEqual(len(allZones), 2) will start flaking if extended-store.sql changes (e.g., adding/removing default zones). Prefer asserting only on presence of zone1.ID/zone2.ID (which you already do) and drop the count assertion.

management/server/store/sql_store.go (2)

4220-4253: GetZoneByDomain inconsistency + misleading not-found error argument.

  • Other zone getters preload Records; GetZoneByDomain doesn’t (surprising API inconsistency).
  • On not found, status.NewZoneNotFoundError(domain) passes a domain string into an error that reads like a zone ID.
Proposed diff
 func (s *SqlStore) GetZoneByDomain(ctx context.Context, accountID, domain string) (*zones.Zone, error) {
 	var zone *zones.Zone
-	result := s.db.Where("account_id = ? AND domain = ?", accountID, domain).First(&zone)
+	result := s.db.
+		Preload("Records").
+		Where("account_id = ? AND domain = ?", accountID, domain).
+		First(&zone)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
-			return nil, status.NewZoneNotFoundError(domain)
+			// Consider introducing a dedicated error helper for "domain not found".
+			return nil, status.Errorf(status.NotFound, "zone domain: %s not found", domain)
 		}
 
 		log.WithContext(ctx).Errorf("failed to get zone by domain from store: %v", result.Error)
 		return nil, status.Errorf(status.Internal, "failed to get zone by domain from store")
 	}
 
 	return zone, nil
 }

4305-4323: GORM usage: Take(&record) with var record *records.Record is easy to get wrong.
This pattern depends on GORM allocating the pointed struct; consider switching to var record records.Record; ...Take(&record) for clarity and to avoid nil-dest edge cases across GORM versions.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 748903a and f39bb66.

📒 Files selected for processing (9)
  • client/internal/dns/server_test.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/internals/shared/grpc/conversion.go
  • management/server/account.go
  • management/server/account_test.go
  • management/server/store/sql_store.go
  • management/server/store/sql_store_test.go
  • management/server/store/store.go
  • shared/management/client/rest/client.go
✅ Files skipped from review due to trivial changes (1)
  • client/internal/dns/server_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/internals/shared/grpc/conversion.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/account.go
  • management/server/store/sql_store_test.go
  • management/server/store/store.go
  • management/server/account_test.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/server/store/sql_store.go
🧬 Code graph analysis (5)
shared/management/client/rest/client.go (2)
shared/management/client/rest/dns.go (1)
  • DNSAPI (12-14)
shared/management/client/rest/dns_zones.go (1)
  • DNSZonesAPI (12-14)
management/server/store/sql_store_test.go (4)
management/server/store/store.go (1)
  • NewTestStoreFromSQL (417-466)
management/internals/modules/zones/zone.go (1)
  • NewZone (24-34)
shared/management/status/error.go (3)
  • Error (54-57)
  • FromError (78-87)
  • NotFound (21-21)
management/internals/modules/zones/records/record.go (4)
  • NewRecord (31-41)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
management/server/store/store.go (3)
shared/management/http/api/types.gen.go (1)
  • Zone (2046-2067)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/internals/controllers/network_map/controller/controller.go (3)
shared/management/proto/management.pb.go (6)
  • NetworkMap (1915-1947)
  • NetworkMap (1962-1962)
  • NetworkMap (1977-1979)
  • CustomZone (2868-2877)
  • CustomZone (2892-2892)
  • CustomZone (2907-2909)
management/server/types/network.go (1)
  • NetworkMap (32-43)
shared/management/http/api/types.gen.go (1)
  • Zone (2046-2067)
management/server/store/sql_store.go (3)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (4)
  • Error (54-57)
  • Errorf (70-75)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Integration (amd64, sqlite)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Darwin
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: Android / Build
  • GitHub Check: release_ui_darwin
  • GitHub Check: FreeBSD Port / Build & Test
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
🔇 Additional comments (29)
management/internals/controllers/network_map/controller/controller.go (9)

23-23: LGTM!

Import correctly added for the new zones module used throughout the controller.


179-179: LGTM!

Clear rename from customZone to peersCustomZone improves readability and distinguishes peer-specific custom zones from account-level zones.


201-206: LGTM!

Account zones are correctly fetched once before iterating over peers, avoiding redundant database calls. Error handling follows the established pattern in this function.


232-236: LGTM!

Both experimental and non-experimental network map paths correctly receive the new peersCustomZone and accountZones parameters, maintaining consistency across code paths.


345-357: LGTM!

Account zones integration follows the same pattern as sendUpdateAccountPeers. Error handling is consistent with other error returns in this function.


450-458: LGTM!

Zone retrieval and custom zone derivation are correctly sequenced. Error handling properly returns the 5-tuple expected by the function signature.


490-508: LGTM!

Function signature properly extended to thread zone data through to the account's network map builder. Based on learnings, empty zones are filtered out in filterPeerAppliedZones, so passing zones here is correct.


822-831: LGTM!

Zone retrieval and custom zone derivation follow the established pattern used in other functions in this file. Error handling correctly returns nil, err.


840-846: LGTM!

Both code paths correctly receive the zone parameters, maintaining consistency with the other network map generation sites in this controller.

shared/management/client/rest/client.go (2)

61-68: DNSZones client wiring is consistent with existing REST sub-APIs.
Nice, this keeps the client surface symmetric (DNS + DNSZones) and discoverable.


108-123: Initialize DNSZones alongside other API groups looks correct.

management/server/account_test.go (2)

400-404: Updated GetPeerNetworkMap call-site looks consistent with the new signature.


1680-1692: Updated GetRoutesToSync call-sites look correct with peer groups passed in.

management/server/store/store.go (1)

25-31: Imports for zones/records are appropriate for the new Store surface.

management/server/store/sql_store_test.go (10)

24-27: Imports look appropriate for the new zone/record store tests.


4054-4107: Good table-driven coverage for GetZoneByID not-found cases.
If empty zoneID should be treated as InvalidArgument (instead of NotFound), add that expectation here once the store behavior is clarified.


4137-4203: Nice coverage of domain lookup and account scoping.
Consider adding a case for domain normalization (e.g., stored example.com, queried Example.COM) if the API intends case-insensitive matching.


4235-4255: DeleteZone test doesn’t exercise the hard case: zone with records.
Given the presence of DeleteZoneDNSRecords, please add a test that creates records, then deletes the zone, and asserts the intended behavior (cascade delete vs expected failure).


4257-4282: DNS record create/get-by-ID assertions are good.
If record names are intended to be relative to the zone (e.g., www instead of www.example.com), align the test data with that contract to avoid encoding a conflicting convention into store tests.


4284-4345: Good table-driven coverage for GetDNSRecordByID not-found cases.
Same note as zones: if empty IDs should be InvalidArgument, reflect it once store behavior is finalized.


4347-4382: GetZoneDNSRecords test is clear and deterministic (new zone ID).


4417-4445: UpdateDNSRecord test is good, but doesn’t cover Type updates / immutability.
If Type is mutable, add an update assertion; if it’s immutable, add a negative test to enforce it (store or manager layer).


4447-4471: DeleteDNSRecord test looks good.


4473-4502: Bulk delete test is good coverage for cleanup APIs.
If DeleteZone is supposed to cascade, this test can also be reused to validate that DeleteZone leaves no orphan records.

management/server/store/sql_store.go (5)

29-32: New imports are fine, but these methods now depend on GORM association behavior.
Please double-check expected gorm association/cascade semantics for zones.Zone.Records in your supported DBs.


123-132: AutoMigrate addition is reasonable; ensure FK constraints behave consistently across engines.
Given zones.Zone has Records []*records.Record, confirm migrations create the expected FK (and ON DELETE behavior) for sqlite/postgres/mysql.


4255-4269: GetAccountZones API looks good (preload + optional locking).


4325-4365: List/query/bulk-delete methods are straightforward; consider not-found semantics.
Right now Find(...) returns ([]*, nil) for “none found” (fine), while “not found” for singletons returns typed errors. Just confirm this is the intended API contract for callers.


4206-4218: No changes needed. The implementation already handles transactional deletion of child records at the manager level (DeleteZone in manager.go), which calls DeleteZoneDNSRecords within a transaction before deleting the zone. The test confirms this behavior.

Likely an incorrect or invalid review comment.

Comment thread management/server/store/sql_store_test.go
Comment thread management/server/store/sql_store_test.go
Comment thread management/server/store/sql_store_test.go
Comment thread management/server/store/sql_store.go
Comment thread management/server/store/sql_store.go
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants